From 3fd6a54622e6497dee25b9db8bd4d30db7fc362d Mon Sep 17 00:00:00 2001 From: garrettmills Date: Mon, 16 Nov 2020 22:48:59 -0600 Subject: [PATCH] Initial form builder support! --- src/app/app.component.html | 7 +- src/app/app.component.ts | 50 ++- src/app/components/components.module.ts | 8 + .../renderers/page-link-renderer.component.ts | 7 +- .../node-picker/node-picker.component.html | 40 ++- .../node-picker/node-picker.component.scss | 6 + .../node-picker/node-picker.component.ts | 3 +- .../form-input/form-input.component.html | 100 ++++++ .../form-input/form-input.component.scss | 0 .../nodes/form-input/form-input.component.ts | 130 ++++++++ .../options/form-input-options.component.ts | 313 ++++++++++++++++++ src/app/pages/editor/editor.page.html | 21 +- src/app/pages/editor/editor.page.scss | 4 + src/app/pages/editor/editor.page.ts | 5 + src/app/service/editor.service.ts | 4 + src/app/structures/HostRecord.ts | 10 +- src/app/structures/PageRecord.ts | 4 +- src/app/structures/db-api.ts | 2 + src/app/structures/node-types.ts | 29 +- src/global.scss | 32 ++ 20 files changed, 743 insertions(+), 32 deletions(-) create mode 100644 src/app/components/nodes/form-input/form-input.component.html create mode 100644 src/app/components/nodes/form-input/form-input.component.scss create mode 100644 src/app/components/nodes/form-input/form-input.component.ts create mode 100644 src/app/components/nodes/form-input/options/form-input-options.component.ts diff --git a/src/app/app.component.html b/src/app/app.component.html index d5105a4..e7543aa 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -13,11 +13,8 @@ - - - - -  Child + +  Create diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 78a581c..be313ee 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -241,6 +241,51 @@ export class AppComponent implements OnInit { window.open(dlUrl, '_blank'); } + async onCreateClick($event: MouseEvent) { + const menuItems = [ + { + name: 'Top-Level Note', + icon: 'fa fa-sticky-note noded-note', + value: 'top-level', + title: 'Create a new top-level note page', + }, + ...(this.addChildTarget ? [ + { + name: 'Child Note', + icon: 'fa fa-sticky-note noded-note', + value: 'child', + title: 'Create a note page as a child of the given note', + }, + { + name: 'Form', + icon: 'fa fa-clipboard-list noded-form', + value: 'form', + title: 'Create a new form page as a child of the given note', + }, + ] : []), + ]; + + const popover = await this.popover.create({ + event: $event, + component: OptionMenuComponent, + componentProps: { + menuItems, + }, + }); + + popover.onDidDismiss().then(({ data: value }) => { + if ( value === 'top-level' ) { + this.onTopLevelCreate(); + } else if ( value === 'child' ) { + this.onChildCreate(); + } else if ( value === 'form' ) { + this.onChildCreate('form'); + } + }); + + await popover.present(); + } + async onTopLevelCreate() { const alert = await this.alerts.create({ header: 'Create Page', @@ -273,7 +318,7 @@ export class AppComponent implements OnInit { await alert.present(); } - async onChildCreate() { + async onChildCreate(pageType?: string) { const alert = await this.alerts.create({ header: 'Create Sub-Page', message: 'Please enter a new name for the page:', @@ -296,7 +341,8 @@ export class AppComponent implements OnInit { handler: async args => { args = { name: args.name, - parentId: this.addChildTarget.data.id + parentId: this.addChildTarget.data.id, + pageType, }; this.api.post('/page/create-child', args).subscribe(res => { this.reloadMenuItems().subscribe(() => { diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index 50f19c8..1220ee4 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -42,6 +42,8 @@ import {DateTimeFilterComponent} from './editor/database/filters/date-time.filte import {DatabasePageComponent} from './editor/database/database-page.component'; import {PageLinkRendererComponent} from './editor/database/renderers/page-link-renderer.component'; import {PageLinkEditorComponent} from './editor/database/editors/page-link/page-link-editor.component'; +import {FormInputComponent} from './nodes/form-input/form-input.component'; +import {FormInputOptionsComponent} from './nodes/form-input/options/form-input-options.component'; @NgModule({ declarations: [ @@ -77,6 +79,8 @@ import {PageLinkEditorComponent} from './editor/database/editors/page-link/page- DatabasePageComponent, PageLinkRendererComponent, PageLinkEditorComponent, + FormInputComponent, + FormInputOptionsComponent, ], imports: [ CommonModule, @@ -125,6 +129,8 @@ import {PageLinkEditorComponent} from './editor/database/editors/page-link/page- DatabasePageComponent, PageLinkRendererComponent, PageLinkEditorComponent, + FormInputComponent, + FormInputOptionsComponent, ], exports: [ NodePickerComponent, @@ -159,6 +165,8 @@ import {PageLinkEditorComponent} from './editor/database/editors/page-link/page- DatabasePageComponent, PageLinkRendererComponent, PageLinkEditorComponent, + FormInputComponent, + FormInputOptionsComponent, ] }) export class ComponentsModule {} diff --git a/src/app/components/editor/database/renderers/page-link-renderer.component.ts b/src/app/components/editor/database/renderers/page-link-renderer.component.ts index 7cd54a4..1b555ec 100644 --- a/src/app/components/editor/database/renderers/page-link-renderer.component.ts +++ b/src/app/components/editor/database/renderers/page-link-renderer.component.ts @@ -2,18 +2,21 @@ import {ICellRendererAngularComp} from 'ag-grid-angular'; import {Component, HostListener} from '@angular/core'; import {ICellRendererParams} from 'ag-grid-community'; import {NavigationService} from '../../../../service/navigation.service'; +import {NodeTypeIcons} from '../../../../structures/node-types'; @Component({ selector: 'editor-page-link-renderer', template: `
- {{ pageTitle }} + {{ pageTitle }}
`, }) export class PageLinkRendererComponent implements ICellRendererAngularComp { public params: ICellRendererParams; public pageId?: string; public pageTitle?: string; + public pageType?: string; + public typeIcons = NodeTypeIcons; constructor( protected readonly nav: NavigationService, @@ -27,10 +30,10 @@ export class PageLinkRendererComponent implements ICellRendererAngularComp { const page = params._pagesData.find(x => x.id === this.pageId); if ( page ) { this.pageTitle = page.name; + this.pageType = page.type; } } - // @HostListener('click', ['@event.target']) onClick(event) { if ( event.ctrlKey ) { event.stopPropagation(); 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 a319230..7eb1c81 100644 --- a/src/app/components/editor/node-picker/node-picker.component.html +++ b/src/app/components/editor/node-picker/node-picker.component.html @@ -7,16 +7,50 @@ Markdown - + Database - + Code Editor - + Upload Files + + + + + Text Input + + + + Number Input + + + + Password Input + + + + E-Mail Input + + + + Single-Select Input + + + + Multi-Select Input + + + + Paragraph Input + + + + + diff --git a/src/app/components/editor/node-picker/node-picker.component.scss b/src/app/components/editor/node-picker/node-picker.component.scss index f5582b4..970f4a2 100644 --- a/src/app/components/editor/node-picker/node-picker.component.scss +++ b/src/app/components/editor/node-picker/node-picker.component.scss @@ -31,3 +31,9 @@ i { color: var(--noded-background-markdown); } } + +.form-input { + i { + color: var(--noded-background-form); + } +} 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 fa8c4db..40fdc1f 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,4 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, Input, OnInit} from '@angular/core'; import {PopoverController} from '@ionic/angular'; import {NodeTypeIcons} from '../../../structures/node-types'; @@ -8,6 +8,7 @@ import {NodeTypeIcons} from '../../../structures/node-types'; styleUrls: ['./node-picker.component.scss'], }) export class NodePickerComponent implements OnInit { + @Input() formMode = true; public typeIcons = NodeTypeIcons; diff --git a/src/app/components/nodes/form-input/form-input.component.html b/src/app/components/nodes/form-input/form-input.component.html new file mode 100644 index 0000000..ab3999a --- /dev/null +++ b/src/app/components/nodes/form-input/form-input.component.html @@ -0,0 +1,100 @@ + + + {{ this.node.AdditionalData.label }} + + + + + + + + + {{ choice.display }} + + + + + {{ choice.display }} + + + + + Edit + diff --git a/src/app/components/nodes/form-input/form-input.component.scss b/src/app/components/nodes/form-input/form-input.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/nodes/form-input/form-input.component.ts b/src/app/components/nodes/form-input/form-input.component.ts new file mode 100644 index 0000000..7e044e9 --- /dev/null +++ b/src/app/components/nodes/form-input/form-input.component.ts @@ -0,0 +1,130 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {EditorNodeContract} from '../EditorNode.contract'; +import {EditorService} from '../../../service/editor.service'; +import {ModalController} from '@ionic/angular'; +import {FormInputOptionsComponent} from './options/form-input-options.component'; +import {DbApiService} from '../../../service/db-api.service'; + +export interface FormSelectChoiceObject { + display: string; + value: string; +} + +@Component({ + selector: 'editor-node-form-input', + templateUrl: './form-input.component.html', + styleUrls: ['./form-input.component.scss'], +}) +export class FormInputComponent extends EditorNodeContract implements OnInit { + @Input() nodeId: string; + @Input() editorUUID?: string; + @Input() isFilloutMode = false; + + public initialValue: any; + public value: any; + public forceDirty = false; + + // TODO load this from somewhere + public selectChoices: FormSelectChoiceObject[] = []; + + constructor( + public editorService: EditorService, + public readonly modals: ModalController, + public readonly dbApi: DbApiService, + ) { + super(); + } + + public isDark() { + return document.body.classList.contains('dark'); + } + + public get isReadonly(): boolean { + return !this.editorService.canEdit(); + } + + ngOnInit() { + this.editorService = this.editorService.getEditor(this.editorUUID); + this.editorService.registerNodeEditor(this.nodeId, this).then(() => { + console.log('form input node', this.node); + if ( !this.node.AdditionalData ) { + this.node.AdditionalData = {}; + } + + this.initialValue = this.value = this.isFilloutMode ? undefined : this.node.Value.Value; + + if ( this.node.type === 'form_input_select' || this.node.type === 'form_input_multiselect' ) { + if ( this.node.AdditionalData.selectSource === 'local' ) { + this.selectChoices = [...this.node.AdditionalData.selectStaticOptions]; + } else if ( this.node.AdditionalData.selectSource === 'database' ) { + this.loadSelectOptionsFromDatabase(); + } + } + }); + } + + async loadSelectOptionsFromDatabase() { + try { + const db = await this.dbApi.getDatabase(this.node.AdditionalData.selectSourceDatabaseId); + if ( db ) { + const data = await db.data(); + console.log('loaded db data', data, this.node.AdditionalData); + this.selectChoices = data.map(x => { + return { + display: x.data[this.node.AdditionalData.selectDatabaseDisplayColumnId], + value: x.data[this.node.AdditionalData.selectDatabaseValueColumnId], + }; + }); + } + } catch (e: unknown) { + this.selectChoices = []; + } + } + + triggerChangeCheck() { + this.editorService.triggerSave(); + } + + public isDirty(): boolean | Promise { + return this.value !== this.initialValue || this.forceDirty; + } + + // FIXME this isn't saving properly, write Mode = 'form' + public writeChangesToNode(): void | Promise { + this.node.Value.Value = this.value; + this.initialValue = this.value; + } + + public async onEditClick() { + if ( this.isFilloutMode ) { + return; + } + + const modal = await this.modals.create({ + component: FormInputOptionsComponent, + componentProps: { + node: this.nodeRec, + editorUUID: this.editorUUID, + }, + }); + + modal.onDidDismiss().then(result => { + if ( result.data ) { + this.forceDirty = true; + this.editorService.triggerSave(); + + if ( + ( + this.node.type === 'form_input_select' + || this.node.type === 'form_input_multiselect' + ) + && this.node.AdditionalData.selectSource === 'database' + ) { + this.loadSelectOptionsFromDatabase(); + } + } + }); + + await modal.present(); + } +} diff --git a/src/app/components/nodes/form-input/options/form-input-options.component.ts b/src/app/components/nodes/form-input/options/form-input-options.component.ts new file mode 100644 index 0000000..6511e80 --- /dev/null +++ b/src/app/components/nodes/form-input/options/form-input-options.component.ts @@ -0,0 +1,313 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {EditorService} from '../../../../service/editor.service'; +import {ModalController} from '@ionic/angular'; +import HostRecord from '../../../../structures/HostRecord'; +import {debug} from '../../../../utility'; +import {DbApiService} from '../../../../service/db-api.service'; +import {Database, DatabaseColumn} from '../../../../structures/db-api'; +import {NodeTypeIcons} from '../../../../structures/node-types'; + +@Component({ + selector: 'noded-node-form-input-options', + template: ` + + + Edit Form Input + + + + + + + + + + + + + + + + + Label Text + + + + + + + + Placeholder Text + + + + + + + + + + Required + + + + + + + Select Options Source + + Static Options + Database Values + + + + + + + + Source Database + + +
{{ port.name }}
+
+
+
+
+
+ + + + Display Column + + + + + + Value Column + + + + + + +
Options
+ +
+ + + + + Display + + + + + + Value + + + + + + + + +
+ + + + Min Length + + + + + + Max Length + + + + + + + + Min + + + + + + Max + + + + +
+
+ `, +}) +export class FormInputOptionsComponent implements OnInit { + @Input() node: HostRecord; + @Input() editorUUID: string; + + public typeIcons = NodeTypeIcons; + + public label = ''; + public placeholder = ''; + public required = false; + + public minLength?: number; + public maxLength?: number; + + public min?: number; + public max?: number; + + public selectSource?: 'local'|'database'; + public selectDatabaseDisplayColumn?: DatabaseColumn; + public selectDatabaseValueColumn?: DatabaseColumn; + public selectStaticOptions: { display: string, value: string }[] = []; + + public databases: Database[] = []; + public selectedDatabase?: Database; + public selectedDatabaseColumns: DatabaseColumn[] = []; + + constructor( + public editorService: EditorService, + public readonly dbApi: DbApiService, + public readonly modal: ModalController, + ) { } + + async ngOnInit() { + debug('Form input options', this); + this.editorService = this.editorService.getEditor(this.editorUUID); + + this.label = this.node.AdditionalData.label; + this.placeholder = this.node.AdditionalData.placeholder; + this.required = this.node.AdditionalData.required; + + if ( this.node.type === 'form_input_password' || this.node.type === 'form_input_text' ) { + this.minLength = this.node.AdditionalData.minLength; + this.maxLength = this.node.AdditionalData.maxLength; + } + + if ( this.node.type === 'form_input_number' ) { + this.min = this.node.AdditionalData.min; + this.max = this.node.AdditionalData.max; + } + + if ( this.node.type === 'form_input_select' || this.node.type === 'form_input_multiselect' ) { + this.selectSource = this.node.AdditionalData.selectSource; + + this.databases = await this.dbApi.getDatabases(); + + if ( this.selectSource === 'database' ) { + this.selectedDatabase = this.databases.find(x => x.uuid === this.node.AdditionalData.selectSourceDatabaseId); + await this.onSelectDatabaseChange(); + + this.selectDatabaseValueColumn = this.selectedDatabaseColumns.find(x => { + return x.field === this.node.AdditionalData.selectDatabaseValueColumnId; + }); + + this.selectDatabaseDisplayColumn = this.selectedDatabaseColumns.find(x => { + return x.field === this.node.AdditionalData.selectDatabaseDisplayColumnId; + }); + } else if ( this.selectSource === 'local' ) { + this.selectStaticOptions = this.node.AdditionalData.selectStaticOptions || []; + } + } + } + + dismissModal(success = true) { + if ( success ) { + this.node.AdditionalData.label = this.label; + this.node.AdditionalData.placeholder = this.placeholder; + this.node.AdditionalData.required = !!this.required; + } + + if ( this.node.type === 'form_input_password' || this.node.type === 'form_input_text' ) { + this.node.AdditionalData.minLength = this.minLength; + this.node.AdditionalData.maxLength = this.maxLength; + } + + if ( this.node.type === 'form_input_number' ) { + this.node.AdditionalData.min = this.min; + this.node.AdditionalData.max = this.max; + } + + if ( this.node.type === 'form_input_select' || this.node.type === 'form_input_multiselect' ) { + this.node.AdditionalData.selectSource = this.selectSource; + + if ( this.selectSource === 'database' ) { + this.node.AdditionalData.selectSourceDatabaseId = this.selectedDatabase?.uuid; + this.node.AdditionalData.selectDatabaseValueColumnId = this.selectDatabaseValueColumn?.field; + this.node.AdditionalData.selectDatabaseDisplayColumnId = this.selectDatabaseDisplayColumn?.field; + } else if ( this.selectSource === 'local' ) { + this.node.AdditionalData.selectStaticOptions = this.selectStaticOptions; + } + } + + this.modal.dismiss(success); + } + + async onSelectDatabaseChange() { + if ( this.selectedDatabase ) { + this.selectedDatabaseColumns = await this.selectedDatabase.columns(); + } else { + this.selectedDatabaseColumns = []; + } + + this.selectDatabaseDisplayColumn = undefined; + this.selectDatabaseValueColumn = undefined; + } + + onAddSelectOption() { + this.selectStaticOptions.push({ + display: '', + value: '', + }); + } + + onRemoveSelectOption(option: any) { + this.selectStaticOptions = this.selectStaticOptions.filter(x => x !== option); + } +} diff --git a/src/app/pages/editor/editor.page.html b/src/app/pages/editor/editor.page.html index 4fe204c..c15ce19 100644 --- a/src/app/pages/editor/editor.page.html +++ b/src/app/pages/editor/editor.page.html @@ -5,12 +5,16 @@ - +
+ + +
@@ -68,6 +72,9 @@ + + +