diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index 6bea45d..c75dc64 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -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 {} diff --git a/src/app/components/editor/database/columns/columns.component.html b/src/app/components/editor/database/columns/columns.component.html new file mode 100644 index 0000000..e9043ed --- /dev/null +++ b/src/app/components/editor/database/columns/columns.component.html @@ -0,0 +1,46 @@ + + + Manage Database Columns + + + + + + + + + + + + + Add Column + Save + + + + + + Field Label + + + + + + Data Type + + Text + Number + Text-Area + + + + + + + + + + + diff --git a/src/app/components/editor/database/columns/columns.component.scss b/src/app/components/editor/database/columns/columns.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/editor/database/columns/columns.component.ts b/src/app/components/editor/database/columns/columns.component.ts new file mode 100644 index 0000000..aa4255c --- /dev/null +++ b/src/app/components/editor/database/columns/columns.component.ts @@ -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; + } + +} diff --git a/src/app/components/editor/database/database.component.html b/src/app/components/editor/database/database.component.html new file mode 100644 index 0000000..b8864f6 --- /dev/null +++ b/src/app/components/editor/database/database.component.html @@ -0,0 +1,22 @@ +
+ + +  Manage Columns +  Insert Row +  Delete Row +  Sync Records +  Drop Database + + +
+ +
+
diff --git a/src/app/components/editor/database/database.component.scss b/src/app/components/editor/database/database.component.scss new file mode 100644 index 0000000..a11b532 --- /dev/null +++ b/src/app/components/editor/database/database.component.scss @@ -0,0 +1,4 @@ +div.database-wrapper { + border: 2px solid #8c8c8c; + border-radius: 3px; +} diff --git a/src/app/components/editor/database/database.component.ts b/src/app/components/editor/database/database.component.ts new file mode 100644 index 0000000..78378d4 --- /dev/null +++ b/src/app/components/editor/database/database.component.ts @@ -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(); + @Output() requestParentSave = new EventEmitter(); + @Output() requestParentDelete = new EventEmitter(); + @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 { + return new Observable(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 { + return new Observable(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 { + return new Observable(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; + } + }); + } + +} diff --git a/src/app/components/editor/host/host.component.html b/src/app/components/editor/host/host.component.html index 2f3c160..e7cc0f8 100644 --- a/src/app/components/editor/host/host.component.html +++ b/src/app/components/editor/host/host.component.html @@ -23,4 +23,15 @@ [innerHTML]="listLines[i]" > +
+ +
diff --git a/src/app/components/editor/host/host.component.spec.ts b/src/app/components/editor/host/host.component.spec.ts deleted file mode 100644 index 5eb2e63..0000000 --- a/src/app/components/editor/host/host.component.spec.ts +++ /dev/null @@ -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; - - 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(); - }); -}); diff --git a/src/app/components/editor/host/host.component.ts b/src/app/components/editor/host/host.component.ts index a6e0e25..9aeb144 100644 --- a/src/app/components/editor/host/host.component.ts +++ b/src/app/components/editor/host/host.component.ts @@ -11,6 +11,7 @@ export class HostComponent implements OnInit { @Output() recordChange = new EventEmitter(); @Output() newHostRequested = new EventEmitter(); @Output() destroyHostRequested = new EventEmitter(); + @Output() saveHostRequested = new EventEmitter(); @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'); diff --git a/src/app/components/editor/node-picker/node-picker.component.html b/src/app/components/editor/node-picker/node-picker.component.html index 8a26120..5a8d183 100644 --- a/src/app/components/editor/node-picker/node-picker.component.html +++ b/src/app/components/editor/node-picker/node-picker.component.html @@ -1,33 +1,33 @@ - + Paragraph - + Heading 1 - + Heading 2 - + Heading 3 - + Heading 4 - + Monospace Block - + Hyperlink - + Database diff --git a/src/app/components/editor/node-picker/node-picker.component.ts b/src/app/components/editor/node-picker/node-picker.component.ts index f972b8d..d65146e 100644 --- a/src/app/components/editor/node-picker/node-picker.component.ts +++ b/src/app/components/editor/node-picker/node-picker.component.ts @@ -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(() => {}); + } + } diff --git a/src/app/pages/editor/editor.page.html b/src/app/pages/editor/editor.page.html index f01e7ba..3e6416a 100644 --- a/src/app/pages/editor/editor.page.html +++ b/src/app/pages/editor/editor.page.html @@ -18,14 +18,15 @@
- +
- Add Node - Save + Add Node + Save
diff --git a/src/app/pages/editor/editor.page.ts b/src/app/pages/editor/editor.page.ts index c40ada4..631431b 100644 --- a/src/app/pages/editor/editor.page.ts +++ b/src/app/pages/editor/editor.page.ts @@ -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(''); diff --git a/src/app/service/page.service.ts b/src/app/service/page.service.ts index a6580f0..f440d63 100644 --- a/src/app/service/page.service.ts +++ b/src/app/service/page.service.ts @@ -52,6 +52,7 @@ export class PageService { } save_nodes(page: PageRecord, nodes: Array): Observable> { + console.log('save nodes', {nodes}) return new Observable>(sub => { nodes = nodes.map(x => { x.PageId = page.UUID; diff --git a/src/app/structures/HostRecord.ts b/src/app/structures/HostRecord.ts index 56b6af3..e78eb8b 100644 --- a/src/app/structures/HostRecord.ts +++ b/src/app/structures/HostRecord.ts @@ -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;