Initial form builder support!
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

This commit is contained in:
Garrett Mills 2020-11-16 22:48:59 -06:00
parent a8f8c0ebf1
commit 3fd6a54622
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
20 changed files with 743 additions and 32 deletions

View File

@ -13,11 +13,8 @@
<ion-button fill="outline" [color]="refreshingMenu ? 'success' : 'light'" (click)="onMenuRefresh()">
<ion-icon color="tertiary" name="refresh"></ion-icon>
</ion-button>
<ion-button fill="outline" color="light" (click)="onTopLevelCreate()">
<ion-icon color="primary" name="add-circle"></ion-icon>
</ion-button>
<ion-button fill="outline" color="light" (click)="onChildCreate()" [disabled]="!addChildTarget">
<ion-icon color="primary" name="add-circle"></ion-icon>&nbsp;<span class="button-text">Child</span>
<ion-button fill="outline" color="light" (click)="onCreateClick($event)">
<ion-icon color="primary" name="add-circle"></ion-icon>&nbsp;<span class="button-text">Create</span>
</ion-button>
<ion-button fill="outline" color="light" (click)="onDeleteClick()" [disabled]="!deleteTarget">
<ion-icon color="danger" name="trash"></ion-icon>

View File

@ -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(() => {

View File

@ -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 {}

View File

@ -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: `
<div title="Control-click to open this page" (click)="onClick($event)" *ngIf="pageId">
<i class="fa fa-sticky-note" style="margin-right: 5px; color: var(--noded-background-note);"></i> {{ pageTitle }}
<i [ngClass]="typeIcons[pageType || 'page']" style="margin-right: 5px;"></i> {{ pageTitle }}
</div>`,
})
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();

View File

@ -7,16 +7,50 @@
<i class="fa" slot="start" [ngClass]="typeIcons.markdown"></i>
<ion-label>Markdown</ion-label>
</ion-item>
<ion-item button (click)="onSelect('database_ref')" class="db">
<ion-item button (click)="onSelect('database_ref')" class="db" *ngIf="!formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.db"></i>
<ion-label>Database</ion-label>
</ion-item>
<ion-item button (click)="onSelect('code_ref')" class="code">
<ion-item button (click)="onSelect('code_ref')" class="code" *ngIf="!formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.code"></i>
<ion-label>Code Editor</ion-label>
</ion-item>
<ion-item button (click)="onSelect('file_ref')" class="files">
<ion-item button (click)="onSelect('file_ref')" class="files" *ngIf="!formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.files"></i>
<ion-label>Upload Files</ion-label>
</ion-item>
<!-- Form inputs -->
<ion-item button (click)="onSelect('form_input_text')" class="form-input form-input-text" *ngIf="formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.form_input_text"></i>
<ion-label>Text Input</ion-label>
</ion-item>
<ion-item button (click)="onSelect('form_input_number')" class="form-input form-input-number" *ngIf="formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.form_input_number"></i>
<ion-label>Number Input</ion-label>
</ion-item>
<ion-item button (click)="onSelect('form_input_password')" class="form-input form-input-password" *ngIf="formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.form_input_password"></i>
<ion-label>Password Input</ion-label>
</ion-item>
<ion-item button (click)="onSelect('form_input_email')" class="form-input form-input-email" *ngIf="formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.form_input_email"></i>
<ion-label>E-Mail Input</ion-label>
</ion-item>
<ion-item button (click)="onSelect('form_input_select')" class="form-input form-input-select" *ngIf="formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.form_input_select"></i>
<ion-label>Single-Select Input</ion-label>
</ion-item>
<ion-item button (click)="onSelect('form_input_multiselect')" class="form-input form-input-multiselect" *ngIf="formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.form_input_multiselect"></i>
<ion-label>Multi-Select Input</ion-label>
</ion-item>
<ion-item button (click)="onSelect('form_input_textarea')" class="form-input form-input-textarea" *ngIf="formMode">
<i class="fa" slot="start" [ngClass]="typeIcons.form_input_textarea"></i>
<ion-label>Paragraph Input</ion-label>
</ion-item>
<!-- <ion-item button (click)="onSelect('form_input_wysiwyg')" class="form-input form-input-wysiwyg">-->
<!-- <i class="fa" slot="start" [ngClass]="typeIcons.form_input_wysiwyg"></i>-->
<!-- <ion-label>Rich-Text Input</ion-label>-->
<!-- </ion-item>-->
</ion-list>

View File

@ -31,3 +31,9 @@ i {
color: var(--noded-background-markdown);
}
}
.form-input {
i {
color: var(--noded-background-form);
}
}

View File

@ -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;

View File

@ -0,0 +1,100 @@
<ion-item
*ngIf="this.node
&& (
this.node.type === 'form_input_text'
|| this.node.type === 'form_input_number'
|| this.node.type === 'form_input_password'
|| this.node.type === 'form_input_email'
|| this.node.type === 'form_input_select'
|| this.node.type === 'form_input_multiselect'
|| this.node.type === 'form_input_textarea'
)"
>
<ion-label [position]="'floating'" *ngIf="this.node.AdditionalData.label">
{{ this.node.AdditionalData.label }}
</ion-label>
<ion-input
*ngIf="this.node.type === 'form_input_text'"
[(ngModel)]="value"
[placeholder]="this.node.AdditionalData.placeholder"
[required]="this.node.AdditionalData.required"
[readonly]="this.isReadonly"
(ionChange)="triggerChangeCheck()"
></ion-input>
<ion-input
*ngIf="this.node.type === 'form_input_number'"
[(ngModel)]="value"
[type]="'number'"
[placeholder]="this.node.AdditionalData.placeholder"
[required]="this.node.AdditionalData.required"
[readonly]="this.isReadonly"
(ionChange)="triggerChangeCheck()"
></ion-input>
<ion-input
*ngIf="this.node.type === 'form_input_password'"
[(ngModel)]="value"
[type]="'password'"
[placeholder]="this.node.AdditionalData.placeholder"
[required]="this.node.AdditionalData.required"
[readonly]="this.isReadonly"
(ionChange)="triggerChangeCheck()"
></ion-input>
<ion-input
*ngIf="this.node.type === 'form_input_email'"
[(ngModel)]="value"
[type]="'email'"
[placeholder]="this.node.AdditionalData.placeholder"
[required]="this.node.AdditionalData.required"
[readonly]="this.isReadonly"
(ionChange)="triggerChangeCheck()"
></ion-input>
<!-- TODO readonly handling -->
<ion-select
*ngIf="this.node.type === 'form_input_select'"
[(ngModel)]="value"
[placeholder]="this.node.AdditionalData.placeholder"
[required]="this.node.AdditionalData.required"
(ionChange)="triggerChangeCheck()"
style="width: 100%;"
>
<ion-select-option
*ngFor="let choice of selectChoices"
[value]="choice.value"
>{{ choice.display }}</ion-select-option>
</ion-select>
<!-- TODO readonly handling -->
<ion-select
*ngIf="this.node.type === 'form_input_multiselect'"
[(ngModel)]="value"
[placeholder]="this.node.AdditionalData.placeholder"
[required]="this.node.AdditionalData.required"
(ionChange)="triggerChangeCheck()"
[multiple]="true"
style="width: 100%;"
>
<ion-select-option
*ngFor="let choice of selectChoices"
[value]="choice.value"
>{{ choice.display }}</ion-select-option>
</ion-select>
<ion-textarea
*ngIf="this.node.type === 'form_input_textarea'"
[(ngModel)]="value"
[placeholder]="this.node.AdditionalData.placeholder"
[required]="this.node.AdditionalData.required"
(ionChange)="triggerChangeCheck()"
[autoGrow]="true"
style="width: 100%;"
[readonly]="this.isReadonly"
></ion-textarea>
<ion-button
slot="end"
style="margin-top: 5px;"
*ngIf="!this.isReadonly && !this.isFilloutMode"
(click)="onEditClick()"
>Edit</ion-button>
</ion-item>

View File

@ -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<boolean> {
return this.value !== this.initialValue || this.forceDirty;
}
// FIXME this isn't saving properly, write Mode = 'form'
public writeChangesToNode(): void | Promise<void> {
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();
}
}

View File

@ -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: `
<ion-header>
<ion-toolbar>
<ion-title>Edit Form Input</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismissModal(true)">
<ion-icon name="save"></ion-icon>
</ion-button>
<ion-button (click)="dismissModal(false)">
<ion-icon name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-grid>
<ion-row>
<ion-col size="12">
<ion-item>
<ion-label [position]="'floating'">Label Text</ion-label>
<ion-input [(ngModel)]="label"></ion-input>
</ion-item>
</ion-col>
</ion-row>
<ion-row>
<ion-col size="12">
<ion-item>
<ion-label [position]="'floating'">Placeholder Text</ion-label>
<ion-input [(ngModel)]="placeholder"></ion-input>
</ion-item>
</ion-col>
</ion-row>
<ion-row>
<ion-col size="12">
<ion-item>
<ion-checkbox slot="start" [(ngModel)]="required">
</ion-checkbox>
Required
</ion-item>
</ion-col>
</ion-row>
<ion-row *ngIf="node.type === 'form_input_select' || node.type === 'form_input_multiselect'">
<ion-col size="12" [title]="'Defines where the options for the select field should come from'">
<ion-item>
<ion-label [position]="'floating'">Select Options Source</ion-label>
<ion-select
[(ngModel)]="selectSource"
style="width: 100%;"
>
<ion-select-option value="local">Static Options</ion-select-option>
<ion-select-option value="database">Database Values</ion-select-option>
</ion-select>
</ion-item>
</ion-col>
</ion-row>
<ion-row
*ngIf="
(node.type === 'form_input_select' || node.type === 'form_input_multiselect')
&& this.selectSource === 'database'"
>
<ion-col size="12">
<ion-item>
<ion-label position="floating">Source Database</ion-label>
<ionic-selectable
[items]="databases"
itemTextField="name"
itemValueField="uuid"
[canSearch]="true"
[title]="'Select a source database'"
[(ngModel)]="selectedDatabase"
(onChange)="onSelectDatabaseChange()"
>
<ng-template ionicSelectableItemTemplate let-port="item" let-isPortSelected="itemIsSelected">
<div><i [ngClass]="typeIcons.db" style="margin-right: 15px;"></i>{{ port.name }}</div>
</ng-template>
</ionic-selectable>
</ion-item>
</ion-col>
</ion-row>
<ion-row
*ngIf="
(node.type === 'form_input_select' || node.type === 'form_input_multiselect')
&& this.selectSource === 'database' && this.selectedDatabase"
>
<ion-col size="6">
<ion-item>
<ion-label position="floating">Display Column</ion-label>
<ionic-selectable
[items]="selectedDatabaseColumns"
itemTextField="name"
itemValueField="uuid"
[canSearch]="true"
[title]="'Select the column to be used as the display value'"
[(ngModel)]="selectDatabaseDisplayColumn"
></ionic-selectable>
</ion-item>
</ion-col>
<ion-col size="6">
<ion-item>
<ion-label position="floating">Value Column</ion-label>
<ionic-selectable
[items]="selectedDatabaseColumns"
itemTextField="name"
itemValueField="uuid"
[canSearch]="true"
[title]="'Select the column to be used as the record value'"
[(ngModel)]="selectDatabaseValueColumn"
></ionic-selectable>
</ion-item>
</ion-col>
</ion-row>
<ion-row
*ngIf="
(node.type === 'form_input_select' || node.type === 'form_input_multiselect')
&& this.selectSource === 'local'"
>
<ion-header style="display: flex; padding-top: 20px;">
<h5 style="flex: 1;">Options</h5>
<button [title]="'Add new option'" (click)="onAddSelectOption()">
<i class="fa fa-plus"></i>
</button>
</ion-header>
<ion-grid>
<ion-row *ngFor="let option of selectStaticOptions">
<ion-col size="5">
<ion-item>
<ion-label position="floating">Display</ion-label>
<ion-input [(ngModel)]="option.display"></ion-input>
</ion-item>
</ion-col>
<ion-col size="5">
<ion-item>
<ion-label position="floating">Value</ion-label>
<ion-input [(ngModel)]="option.value"></ion-input>
</ion-item>
</ion-col>
<ion-col size="2">
<button [title]="'Remove this option'" style="height: 100px; padding: 10px; color: red;" (click)="onRemoveSelectOption(option)">
<i class="fa fa-trash"></i>
</button>
</ion-col>
</ion-row>
</ion-grid>
</ion-row>
<ion-row *ngIf="node.type === 'form_input_password' || node.type === 'form_input_text'">
<ion-col size="6">
<ion-item>
<ion-label [position]="'floating'">Min Length</ion-label>
<ion-input type="'number'" [(ngModel)]="minLength"></ion-input>
</ion-item>
</ion-col>
<ion-col size="6">
<ion-item>
<ion-label [position]="'floating'">Max Length</ion-label>
<ion-input type="'number'" [(ngModel)]="maxLength"></ion-input>
</ion-item>
</ion-col>
</ion-row>
<ion-row *ngIf="node.type === 'form_input_number'">
<ion-col size="6">
<ion-item>
<ion-label [position]="'floating'">Min</ion-label>
<ion-input type="'number'" [(ngModel)]="min"></ion-input>
</ion-item>
</ion-col>
<ion-col size="6">
<ion-item>
<ion-label [position]="'floating'">Max</ion-label>
<ion-input type="'number'" [(ngModel)]="max"></ion-input>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
`,
})
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);
}
}

View File

@ -5,12 +5,16 @@
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title #titleBar>
<ion-input
[(ngModel)]="editorService.mutablePageName"
[readonly]="!editorService.canEdit()"
placeholder="Click to edit page name..."
class="title-input"
></ion-input>
<div style="display: flex; flex-direction: row;">
<i *ngIf="editorService.currentPageType" [ngClass]="typeIcons[editorService.currentPageType]" style="font-size: 18pt; margin: auto 10px;"></i>
<ion-input
style="flex: 1;"
[(ngModel)]="editorService.mutablePageName"
[readonly]="!editorService.canEdit()"
placeholder="Click to edit page name..."
class="title-input"
></ion-input>
</div>
</ion-title>
<ion-buttons slot="end">
<button
@ -48,7 +52,7 @@
*ngFor="let node of editorService.immutableNodes"
>
<div class="host-icons">
<i class="type-icon fa" [ngClass]="typeIcons[(node.isNorm() ? 'node' : node.type)] + ' ' + (node.isNorm() ? 'node' : node.type)"></i>
<i class="type-icon fa" [ngClass]="typeIcons[(node.isNorm() ? 'node' : node.type)] + ' ' + (node.isNorm() ? 'node' : (node.isForm() ? 'form-input ' + node.type : node.type))"></i>
<button (click)="onOptionsClick($event, node)" *ngIf="editorService.canEdit()">
<i class="fa fa-ellipsis-v" title="Node options"></i>
</button>
@ -68,6 +72,9 @@
<ng-container *ngIf="node.type === 'file_ref'">
<editor-files style="flex: 1;" [nodeId]="node.UUID" [editorUUID]="editorService.instanceUUID"></editor-files>
</ng-container>
<ng-container *ngIf="node.isForm()">
<editor-node-form-input style="flex: 1;" [nodeId]="node.UUID" [editorUUID]="editorService.instanceUUID"></editor-node-form-input>
</ng-container>
</div>
<button *ngIf="editorService.canEdit()" class="host-add-button" (click)="onAddClick($event)">
<i class="fa fa-plus"></i> Add Node

View File

@ -36,6 +36,10 @@ ion-icon.invisible {
&.markdown {
color: var(--noded-background-markdown);
}
&.form-input {
color: var(--noded-background-form);
}
}
.host-add-button {

View File

@ -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 => {

View File

@ -95,6 +95,10 @@ export class EditorService {
}
}
public get currentPageType() {
return this.currentPage?.PageType;
}
constructor(
protected api: ApiService,
protected nav: NavigationService,

View File

@ -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];

View File

@ -22,6 +22,7 @@ export default class PageRecord {
public UpdateUserId: string;
public ChildPageIds: Array<string>;
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];

View File

@ -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;
}
}

View File

@ -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',
};

View File

@ -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 {