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 @@ + + + Add Node diff --git a/src/app/pages/editor/editor.page.scss b/src/app/pages/editor/editor.page.scss index 0d7df1d..7b00075 100644 --- a/src/app/pages/editor/editor.page.scss +++ b/src/app/pages/editor/editor.page.scss @@ -36,6 +36,10 @@ ion-icon.invisible { &.markdown { color: var(--noded-background-markdown); } + + &.form-input { + color: var(--noded-background-form); + } } .host-add-button { diff --git a/src/app/pages/editor/editor.page.ts b/src/app/pages/editor/editor.page.ts index 4936438..39d696f 100644 --- a/src/app/pages/editor/editor.page.ts +++ b/src/app/pages/editor/editor.page.ts @@ -20,6 +20,8 @@ export class EditorPage implements OnInit { @Input() hosted = false; @Input() version?: number; + public pageType?: string; + @Input() set readonly(val: boolean) { this.editorService.forceReadonly = val; @@ -119,6 +121,9 @@ export class EditorPage implements OnInit { const popover = await this.popover.create({ component: NodePickerComponent, event, + componentProps: { + formMode: this.editorService.currentPageType === 'form', + }, }); popover.onDidDismiss().then(result => { diff --git a/src/app/service/editor.service.ts b/src/app/service/editor.service.ts index e273343..19a1437 100644 --- a/src/app/service/editor.service.ts +++ b/src/app/service/editor.service.ts @@ -95,6 +95,10 @@ export class EditorService { } } + public get currentPageType() { + return this.currentPage?.PageType; + } + constructor( protected api: ApiService, protected nav: NavigationService, diff --git a/src/app/structures/HostRecord.ts b/src/app/structures/HostRecord.ts index a53453d..291a00e 100644 --- a/src/app/structures/HostRecord.ts +++ b/src/app/structures/HostRecord.ts @@ -1,12 +1,14 @@ export default class HostRecord { public value = ''; - public type: 'paragraph'|'database_ref'|'code_ref'|'file_ref'|'markdown' = 'paragraph'; + // tslint:disable-next-line:max-line-length + public type: 'paragraph'|'database_ref'|'code_ref'|'file_ref'|'markdown'|'form_input_text'|'form_input_number'|'form_input_password'|'form_input_email'|'form_input_select'|'form_input_multiselect'|'form_input_textarea' = 'paragraph'; public CreatedAt: string; public PageId: string; private privUUID: string; public UpdatedAt: string; public Value: any; + public AdditionalData: any; public get UUID(): string { return this.privUUID; @@ -22,6 +24,10 @@ export default class HostRecord { return ['paragraph', 'header1', 'header2', 'header3', 'header4', 'block_code', 'click_link', 'page_sep'].includes(this.type); } + public isForm() { + return !!this.type?.toLowerCase()?.startsWith('form_input_'); + } + load(data: any) { this.type = data.Type; this.privUUID = data.UUID; @@ -31,6 +37,7 @@ export default class HostRecord { 'PageId', 'UpdatedAt', 'Value', + 'AdditionalData', ].forEach(field => { if ( field in data ) { this[field] = data[field]; @@ -49,6 +56,7 @@ export default class HostRecord { 'UUID', 'UpdatedAt', 'Value', + 'AdditionalData', ].forEach(field => { if ( field in this ) { data[field] = this[field]; diff --git a/src/app/structures/PageRecord.ts b/src/app/structures/PageRecord.ts index 0ea0bf8..d45bd0b 100644 --- a/src/app/structures/PageRecord.ts +++ b/src/app/structures/PageRecord.ts @@ -22,6 +22,7 @@ export default class PageRecord { public UpdateUserId: string; public ChildPageIds: Array; public level: 'view'|'manage'|'update'|false; + public PageType: 'page' | 'form' = 'page'; constructor(data: any = {Name: 'Click to edit...'}) { [ @@ -37,7 +38,8 @@ export default class PageRecord { 'CreatedUserId', 'UpdateUserId', 'ChildPageIds', - 'level' + 'level', + 'PageType', ].forEach(field => { if ( field in data ) { this[field] = data[field]; diff --git a/src/app/structures/db-api.ts b/src/app/structures/db-api.ts index 8fcd37d..6094d6f 100644 --- a/src/app/structures/db-api.ts +++ b/src/app/structures/db-api.ts @@ -66,6 +66,7 @@ export class DatabaseColumn { public databaseId!: string; public uuid!: string; public type!: string; + public field!: string; public metadata: any; constructor(record?: any) { @@ -79,6 +80,7 @@ export class DatabaseColumn { this.databaseId = record.database_id; this.uuid = record.uuid; this.type = record.type; + this.field = record.field; this.metadata = record.metadata; } } diff --git a/src/app/structures/node-types.ts b/src/app/structures/node-types.ts index c21b1bd..b361a67 100644 --- a/src/app/structures/node-types.ts +++ b/src/app/structures/node-types.ts @@ -1,13 +1,22 @@ export const NodeTypeIcons = { branch: 'fa fa-folder', - node: 'fa fa-quote-left', - norm: 'fa fa-quote-left', - page: 'fa fa-sticky-note', - db: 'fa fa-database', - database_ref: 'fa fa-database', - code: 'fa fa-code', - code_ref: 'fa fa-code', - file_ref: 'fa fa-archive', - files: 'fa fa-archive', - markdown: 'fab fa-markdown', + node: 'fa fa-quote-left noded-node', + norm: 'fa fa-quote-left noded-node', + form: 'fa fa-clipboard-list noded-form', + page: 'fa fa-sticky-note noded-note', + db: 'fa fa-database noded-db', + database_ref: 'fa fa-database noded-db', + code: 'fa fa-code noded-code', + code_ref: 'fa fa-code noded-code', + file_ref: 'fa fa-archive noded-files', + files: 'fa fa-archive noded-files', + markdown: 'fab fa-markdown noded-markdown', + form_input_text: 'fa fa-font noded-form', + form_input_number: 'fa fa-hashtag noded-form', + form_input_password: 'fa fa-key noded-form', + form_input_email: 'fa fa-envelope noded-form', + form_input_select: 'fa fa-check noded-form', + form_input_multiselect: 'fa fa-check-double noded-form', + form_input_textarea: 'fa fa-paragraph noded-form', + form_input_wysiwyg: 'fa fa-quote-right noded-form', }; diff --git a/src/global.scss b/src/global.scss index 4478590..680c277 100644 --- a/src/global.scss +++ b/src/global.scss @@ -56,6 +56,38 @@ --noded-background-markdown: #5F4D30; --noded-color-markdown: white; --noded-color-markdown-hover: #7A633E; + + --noded-background-form: #F2C57C; + --noded-color-form: white; + --noded-background-form-hover: #F8DEB5; +} + +.noded-note { + color: var(--noded-background-note); +} + +.noded-db { + color: var(--noded-background-db); +} + +.noded-node { + color: var(--noded-background-node); +} + +.noded-code { + color: var(--noded-background-code); +} + +.noded-files { + color: var(--noded-background-files); +} + +.noded-markdown { + color: var(--noded-background-markdown); +} + +.noded-form { + color: var(--noded-background-form); } div.picker-wrapper {