Initial form builder support!
This commit is contained in:
parent
a8f8c0ebf1
commit
3fd6a54622
@ -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> <span class="button-text">Child</span>
|
||||
<ion-button fill="outline" color="light" (click)="onCreateClick($event)">
|
||||
<ion-icon color="primary" name="add-circle"></ion-icon> <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>
|
||||
|
@ -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(() => {
|
||||
|
@ -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 {}
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -31,3 +31,9 @@ i {
|
||||
color: var(--noded-background-markdown);
|
||||
}
|
||||
}
|
||||
|
||||
.form-input {
|
||||
i {
|
||||
color: var(--noded-background-form);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
100
src/app/components/nodes/form-input/form-input.component.html
Normal file
100
src/app/components/nodes/form-input/form-input.component.html
Normal 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>
|
130
src/app/components/nodes/form-input/form-input.component.ts
Normal file
130
src/app/components/nodes/form-input/form-input.component.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -5,12 +5,16 @@
|
||||
<ion-menu-button></ion-menu-button>
|
||||
</ion-buttons>
|
||||
<ion-title #titleBar>
|
||||
<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
|
||||
|
@ -36,6 +36,10 @@ ion-icon.invisible {
|
||||
&.markdown {
|
||||
color: var(--noded-background-markdown);
|
||||
}
|
||||
|
||||
&.form-input {
|
||||
color: var(--noded-background-form);
|
||||
}
|
||||
}
|
||||
|
||||
.host-add-button {
|
||||
|
@ -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 => {
|
||||
|
@ -95,6 +95,10 @@ export class EditorService {
|
||||
}
|
||||
}
|
||||
|
||||
public get currentPageType() {
|
||||
return this.currentPage?.PageType;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected api: ApiService,
|
||||
protected nav: NavigationService,
|
||||
|
@ -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];
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user