finish database implementation

This commit is contained in:
garrettmills 2020-02-08 23:09:46 -06:00
parent e2dd56ab72
commit e97c19f19d
16 changed files with 444 additions and 60 deletions

View File

@ -3,11 +3,15 @@ import { CommonModule } from '@angular/common';
import { HostComponent } from './editor/host/host.component';
import {NodePickerComponent} from './editor/node-picker/node-picker.component';
import {IonicModule} from '@ionic/angular';
import {DatabaseComponent} from './editor/database/database.component';
import {AgGridModule} from 'ag-grid-angular';
import {ColumnsComponent} from './editor/database/columns/columns.component';
import {FormsModule} from '@angular/forms';
@NgModule({
declarations: [HostComponent, NodePickerComponent],
imports: [CommonModule, IonicModule],
entryComponents: [HostComponent, NodePickerComponent],
exports: [HostComponent, NodePickerComponent]
declarations: [HostComponent, NodePickerComponent, DatabaseComponent, ColumnsComponent],
imports: [CommonModule, IonicModule, AgGridModule, FormsModule],
entryComponents: [HostComponent, NodePickerComponent, DatabaseComponent, ColumnsComponent],
exports: [HostComponent, NodePickerComponent, DatabaseComponent, ColumnsComponent]
})
export class ComponentsModule {}

View File

@ -0,0 +1,46 @@
<ion-header>
<ion-toolbar>
<ion-title>Manage Database Columns</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismissModal(false)">
<ion-icon name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-grid>
<ion-row>
<ion-col size="12">
<ion-button (click)="onAddColumnClick()" fill="outline">Add Column</ion-button>
<ion-button (click)="dismissModal(true)" color="success" fill="outline">Save</ion-button>
</ion-col>
</ion-row>
<ion-row
*ngFor="let colSet of columnSets; let i = index"
>
<ion-col size="5">
<ion-item>
<ion-label position="floating">Field Label</ion-label>
<ion-input type="text" required [(ngModel)]="columnSets[i].headerName"></ion-input>
</ion-item>
</ion-col>
<ion-col size="5">
<ion-item>
<ion-label position="floating">Data Type</ion-label>
<ion-select interface="popover" [(ngModel)]="columnSets[i].Type">
<ion-select-option value="text">Text</ion-select-option>
<ion-select-option value="number">Number</ion-select-option>
<ion-select-option value="textarea">Text-Area</ion-select-option>
</ion-select>
</ion-item>
</ion-col>
<ion-col size="2" align-items-center>
<ion-button fill="outline" color="light" (click)="onDeleteClick(i)">
<ion-icon color="danger" name="trash"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@ -0,0 +1,41 @@
import {Component, Input, OnInit} from '@angular/core';
import {ModalController} from '@ionic/angular';
@Component({
selector: 'editor-database-columns',
templateUrl: './columns.component.html',
styleUrls: ['./columns.component.scss'],
})
export class ColumnsComponent implements OnInit {
@Input() columnSets: Array<{headerName: string, field: string, Type: string}> = [];
constructor(
protected modals: ModalController
) { }
ngOnInit() {}
onAddColumnClick() {
this.columnSets.push({headerName: '', field: '', Type: ''});
}
dismissModal(doSave = true) {
if ( doSave ) {
this.columnSets = this.columnSets.map(x => {
x.field = x.headerName;
return x;
})
this.modals.dismiss(this.columnSets);
} else {
this.modals.dismiss();
}
}
onDeleteClick(i) {
const newSets = this.columnSets.filter((x, index) => {
return index !== i;
});
this.columnSets = newSets;
}
}

View File

@ -0,0 +1,22 @@
<div class="database-wrapper">
<ion-toolbar>
<ion-buttons>
<ion-button (click)="onManageColumns()"><ion-icon name="build" color="primary"></ion-icon>&nbsp;Manage Columns</ion-button>
<ion-button (click)="onInsertRow()"><ion-icon name="add-circle" color="success"></ion-icon>&nbsp;Insert Row</ion-button>
<ion-button (click)="onRemoveRow()" [disabled]="lastClickRow < 0"><ion-icon name="remove-circle" color="danger"></ion-icon>&nbsp;Delete Row</ion-button>
<ion-button (click)="onSyncRecords()"><ion-icon name="save" [color]="dirty ? 'warning' : 'success'"></ion-icon>&nbsp;Sync Records</ion-button>
<ion-button (click)="onDropDatabase()"><ion-icon name="alert" color="danger"></ion-icon>&nbsp;Drop Database</ion-button>
</ion-buttons>
</ion-toolbar>
<div class="grid-wrapper">
<ag-grid-angular
style="width: 100%; height: 500px;"
class="ag-theme-balham"
[rowData]="rowData"
[columnDefs]="columnDefs"
(rowClicked)="onRowClicked($event)"
(cellValueChanged)="onCellValueChanged()"
#agGridElement
></ag-grid-angular>
</div>
</div>

View File

@ -0,0 +1,4 @@
div.database-wrapper {
border: 2px solid #8c8c8c;
border-radius: 3px;
}

View File

@ -0,0 +1,219 @@
import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
import HostRecord from '../../../structures/HostRecord';
import {ApiService} from '../../../service/api.service';
import {Observable} from 'rxjs';
import {AlertController, ModalController} from '@ionic/angular';
import {ColumnsComponent} from './columns/columns.component';
import {AgGridAngular} from 'ag-grid-angular';
@Component({
selector: 'editor-database',
templateUrl: './database.component.html',
styleUrls: ['./database.component.scss'],
})
export class DatabaseComponent implements OnInit {
@Input() hostRecord: HostRecord;
@Output() hostRecordChange = new EventEmitter<HostRecord>();
@Output() requestParentSave = new EventEmitter<DatabaseComponent>();
@Output() requestParentDelete = new EventEmitter<DatabaseComponent>();
@ViewChild('agGridElement', {static: false}) agGridElement: AgGridAngular;
public dbRecord: any;
public pendingSetup = true;
public dirty = false;
protected lastClickRow = -1;
constructor(
protected api: ApiService,
protected modals: ModalController,
protected alerts: AlertController,
) { }
title = 'app';
columnDefs = [];
rowData = [];
ngOnInit() {
this.getInitObservable().subscribe(() => {
this.getColumnLoadObservable().subscribe(() => {
this.getDataLoadObservable().subscribe(() => {
});
});
});
}
onCellValueChanged() {
this.dirty = true;
}
async onManageColumns() {
const modal = await this.modals.create({
component: ColumnsComponent,
componentProps: {columnSets: this.columnDefs},
});
modal.onDidDismiss().then(result => {
if ( result.data ) {
this.columnDefs = result.data.map(x => {
x.editable = true;
if ( x.Type === 'text' ) {
x.editor = 'agTextCellEditor';
} else if ( x.Type === 'number' ) {
x.valueFormatter = (value) => {
const num = parseFloat(value.value);
if ( !isNaN(num) ) {
return num;
} else {
return '';
}
};
} else if ( x.Type === 'textarea' ) {
x.editor = 'agPopupTextCellEditor';
}
return x;
});
const endpoint = `/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/set/${this.hostRecord.Value.Value}/columns`
this.api.post(endpoint, {columns: this.columnDefs}).subscribe(res => {
this.columnDefs = res.data;
});
}
});
await modal.present();
}
onInsertRow() {
this.rowData.push({});
this.agGridElement.api.setRowData(this.rowData);
}
async onRemoveRow() {
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: () => {
const newRows = this.rowData.filter((x, i) => {
return i !== this.lastClickRow;
});
this.rowData = newRows;
this.agGridElement.api.setRowData(this.rowData);
this.lastClickRow = -1;
},
}
],
});
await alert.present();
}
async onDropDatabase() {
const alert = await this.alerts.create({
header: 'Are you sure?',
message: `You are about to delete this database and all its entries. This action cannot be undone.`,
buttons: [
{
text: 'Keep It',
role: 'cancel',
},
{
text: 'Delete It',
handler: async () => {
this.api.post(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/drop/${this.hostRecord.Value.Value}`).subscribe();
this.requestParentDelete.emit(this);
},
},
],
});
await alert.present();
}
onRowClicked($event) {
this.lastClickRow = $event.rowIndex;
}
getDataLoadObservable(): Observable<any> {
return new Observable<any>(sub => {
this.api.get(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/get/${this.hostRecord.Value.Value}/data`).subscribe(res => {
this.rowData = res.data.map(x => x.RowData);
this.agGridElement.api.setRowData(this.rowData);
sub.next();
sub.complete();
});
});
}
onSyncRecords() {
this.api.post(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/set/${this.hostRecord.Value.Value}/data`, this.rowData)
.subscribe(res => {
this.rowData = res.data.map(x => x.RowData);
this.agGridElement.api.setRowData(this.rowData);
this.dirty = false;
});
}
getColumnLoadObservable(): Observable<any> {
return new Observable<any>(sub => {
this.api.get(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/get/${this.hostRecord.Value.Value}/columns`).subscribe(res => {
this.columnDefs = res.data.map(x => {
x.editable = true;
if ( x.Type === 'text' ) {
x.editor = 'agTextCellEditor';
} else if ( x.Type === 'number' ) {
x.valueFormatter = (value) => {
const num = parseFloat(value.value);
if ( !isNaN(num) ) {
return num;
} else {
return '';
}
};
} else if ( x.Type === 'textarea' ) {
x.editor = 'agPopupTextCellEditor';
}
return x;
});
sub.next();
sub.complete();
});
});
}
getInitObservable(): Observable<any> {
return new Observable<any>(sub => {
if ( this.hostRecord && this.pendingSetup ) {
if ( !this.hostRecord.Value.Value ) {
this.api.post(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/create`).subscribe(res => {
this.dbRecord = res.data;
this.hostRecord.Value.Mode = 'database';
this.hostRecord.Value.Value = res.data.UUID;
this.hostRecord.value = res.data.UUID;
this.hostRecordChange.emit(this.hostRecord);
this.pendingSetup = false;
sub.next(true);
sub.complete();
});
} else {
this.api.get(`/db/${this.hostRecord.PageId}/${this.hostRecord.UUID}/get/${this.hostRecord.Value.Value}`).subscribe(res => {
this.dbRecord = res.data;
this.pendingSetup = false;
sub.next(true);
sub.complete();
});
}
} else {
this.pendingSetup = true;
}
});
}
}

View File

@ -23,4 +23,15 @@
[innerHTML]="listLines[i]"
></li>
</ul>
<div
*ngIf="record.type === 'database_ref'"
class="db-wrapper"
>
<editor-database
[hostRecord]="record"
(hostRecordChange)="onRecordChange($event)"
(requestParentSave)="onRequestParentSave($event)"
(requestParentDelete)="onRequestDelete($event)"
></editor-database>
</div>
</ng-container>

View File

@ -1,24 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { HostComponent } from './host.component';
describe('HostComponent', () => {
let component: HostComponent;
let fixture: ComponentFixture<HostComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HostComponent ],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(HostComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -11,6 +11,7 @@ export class HostComponent implements OnInit {
@Output() recordChange = new EventEmitter<HostRecord>();
@Output() newHostRequested = new EventEmitter<HostComponent>();
@Output() destroyHostRequested = new EventEmitter<HostComponent>();
@Output() saveHostRequested = new EventEmitter<HostComponent>();
@ViewChild('hostContainer', {static: false}) hostContainer: ElementRef;
@ViewChildren('liItems') liItems;
@ -20,6 +21,11 @@ export class HostComponent implements OnInit {
ngOnInit() {}
onRecordChange($event) {
console.log({$event});
this.recordChange.emit($event);
}
onKeyUp($event) {
const innerText = this.hostContainer.nativeElement.innerText.trim()
if ( $event.code === 'Enter'
@ -45,8 +51,8 @@ export class HostComponent implements OnInit {
this.record.type = 'block_code';
} else if ( innerText.startsWith('http') ) {
this.record.type = 'click_link';
} else if ( innerText.startsWith('-') || innerText.startsWith(' -') ) {
this.record.type = 'ul';
} else if ( false && innerText.startsWith('-') || innerText.startsWith(' -') ) {
// this.record.type = 'ul';
this.listLines = [this.record.value];
setTimeout(() => {
const item = this.liItems.toArray()[0].nativeElement;
@ -57,15 +63,17 @@ export class HostComponent implements OnInit {
s.removeAllRanges();
s.addRange(r);
}, 0);
} else {
this.record.type = 'paragraph';
}
}
onRequestDelete($event) {
this.destroyHostRequested.emit(this);
}
onLIKeyUp($event, i) {
console.log({$event});
if ( $event.code === 'Enter' ) {
const newListLines = [];
/*const newListLines = [];
this.liItems.forEach((li, index) => {
newListLines.push(li.nativeElement.innerText.trim());
if ( index === i ) {
@ -73,7 +81,7 @@ export class HostComponent implements OnInit {
}
});
this.listLines = newListLines;
this.listLines = newListLines;*/
// this.listLines[i] = this.liItems[i].innerText.trim()
// const newLines = []
// this.listLines.forEach((rec, x) => {
@ -98,6 +106,10 @@ export class HostComponent implements OnInit {
}
}
onRequestParentSave($event) {
this.saveHostRequested.emit(this);
}
onHostDblClick() {
if ( this.record.type === 'click_link' ) {
window.open(this.record.value.trim(), '_blank');

View File

@ -1,33 +1,33 @@
<ion-list>
<ion-item>
<ion-item button (click)="onSelect('paragraph')">
<ion-icon slot="start" name="menu"></ion-icon>
<ion-label>Paragraph</ion-label>
</ion-item>
<ion-item>
<ion-item button (click)="onSelect('header1')">
<ion-icon slot="start" name="alert"></ion-icon>
<ion-label>Heading 1</ion-label>
</ion-item>
<ion-item>
<ion-item button (click)="onSelect('header2')">
<ion-icon slot="start" name="alert"></ion-icon>
<ion-label>Heading 2</ion-label>
</ion-item>
<ion-item>
<ion-item button (click)="onSelect('header3')">
<ion-icon slot="start" name="alert"></ion-icon>
<ion-label>Heading 3</ion-label>
</ion-item>
<ion-item>
<ion-item button (click)="onSelect('header4')">
<ion-icon slot="start" name="alert"></ion-icon>
<ion-label>Heading 4</ion-label>
</ion-item>
<ion-item>
<ion-item button (click)="onSelect('block_code')">
<ion-icon slot="start" name="code"></ion-icon>
<ion-label>Monospace Block</ion-label>
</ion-item>
<ion-item>
<ion-item button (click)="onSelect('click_link')">
<ion-icon slot="start" name="link"></ion-icon>
<ion-label>Hyperlink</ion-label>
</ion-item>
<ion-item>
<ion-item button (click)="onSelect('database_ref')">
<ion-icon slot="start" name="analytics"></ion-icon>
<ion-label>Database</ion-label>
</ion-item>

View File

@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core';
import {PopoverController} from '@ionic/angular';
@Component({
selector: 'editor-node-picker',
@ -7,8 +8,14 @@ import { Component, OnInit } from '@angular/core';
})
export class NodePickerComponent implements OnInit {
constructor() { }
constructor(
private popover: PopoverController,
) { }
ngOnInit() {}
onSelect(value: string) {
this.popover.dismiss(value).then(() => {});
}
}

View File

@ -18,14 +18,15 @@
<ng-container>
<div class="editor-root ion-padding">
<div class="host-container ion-padding">
<editor-host #editorHosts *ngFor="let record of hostRecords; let i = index" [(record)]="hostRecords[i]"
(newHostRequested)="onNewHostRequested($event)" (destroyHostRequested)="onDestroyHostRequested($event)">
<editor-host #editorHosts *ngFor="let record of hostRecords; let i = index" [record]="hostRecords[i]" (recordChange)="onHostRecordChange($event, i)"
(newHostRequested)="onNewHostRequested($event)" (destroyHostRequested)="onDestroyHostRequested($event)"
(saveHostRequested)="onSaveClick()">
</editor-host>
</div>
</div>
<div class="editor-buttons">
<ion-button (click)="onAddClick($event)" class="ion-padding ion-margin" fill="outline" color="medium">Add Node</ion-button>
<ion-button (click)="onSaveClick()" class="ion-padding ion-margin" fill="outline" color="medium">Save</ion-button>
<ion-button (click)="onAddClick($event)" class="ion-padding ion-margin-start" fill="outline" color="medium">Add Node</ion-button>
<ion-button (click)="onSaveClick()" class="ion-padding" fill="outline" color="medium">Save</ion-button>
</div>
</ng-container>
</ion-content>

View File

@ -28,6 +28,8 @@ export class EditorPage implements OnInit {
this.route.params.subscribe(params => {
this.pageId = params.id;
});
console.log('editor page', this);
}
ngOnInit() {}
@ -45,18 +47,9 @@ export class EditorPage implements OnInit {
}
}
/*onAddClick() {
this.hostRecords.push(new HostRecord(''));
setTimeout(() => {
const host = this.editorHosts.toArray().reverse()[0].hostContainer.nativeElement;
const s = window.getSelection();
const r = document.createRange();
r.setStart(host, 0);
r.setEnd(host, 0);
s.removeAllRanges();
s.addRange(r);
}, 0);
}*/
onHostRecordChange($event, i) {
this.hostRecords[i] = $event;
}
async onAddClick($event) {
const popover = await this.popover.create({
@ -64,9 +57,52 @@ export class EditorPage implements OnInit {
event: $event,
});
popover.onDidDismiss().then(arg => {
console.log({arg});
const defValue = this.getDefaultValue(arg.data);
const hostRec = new HostRecord(defValue);
console.log({hostRec});
hostRec.type = arg.data;
hostRec.PageId = this.pageRecord.UUID;
this.hostRecords.push(hostRec);
if ( hostRec.isNorm() ) {
setTimeout(() => {
const host = this.editorHosts.toArray().reverse()[0].hostContainer.nativeElement;
const s = window.getSelection();
const r = document.createRange();
r.setStart(host, defValue.length);
r.setEnd(host, defValue.length);
s.removeAllRanges();
s.addRange(r);
}, 0);
} else {
this.onSaveClick();
}
});
await popover.present();
}
getDefaultValue(type: string) {
if ( type === 'paragraph' ) {
return '';
} else if ( type === 'header1' ) {
return '# ';
} else if ( type === 'header2' ) {
return '## ';
} else if ( type === 'header3' ) {
return '### ';
} else if ( type === 'header4' ) {
return '#### ';
} else if ( type === 'block_code' ) {
return '```';
} else if ( type === 'click_link' ) {
return 'https://';
} else {
return '';
}
}
onNewHostRequested($event) {
const insertAfter = this.getIndexFromRecord($event.record);
const record = new HostRecord('');

View File

@ -52,6 +52,7 @@ export class PageService {
}
save_nodes(page: PageRecord, nodes: Array<HostRecord>): Observable<Array<HostRecord>> {
console.log('save nodes', {nodes})
return new Observable<Array<HostRecord>>(sub => {
nodes = nodes.map(x => {
x.PageId = page.UUID;

View File

@ -1,6 +1,6 @@
export default class HostRecord {
public value = '';
public type: 'paragraph'|'header1'|'header2'|'header3'|'header4'|'block_code'|'click_link'|'ul' = 'paragraph';
public type: 'paragraph'|'header1'|'header2'|'header3'|'header4'|'block_code'|'click_link'|'database_ref' = 'paragraph';
public CreatedAt: string;
public PageId: string;
@ -12,6 +12,10 @@ export default class HostRecord {
this.value = value;
}
public isNorm() {
return ['paragraph', 'header1', 'header2', 'header3', 'header4', 'block_code', 'click_link'].includes(this.type);
}
load(data: any) {
this.type = data.Type;