Compare commits

..

No commits in common. 'master' and 'Brokenbuttons' have entirely different histories.

@ -1,58 +0,0 @@
---
kind: pipeline
type: kubernetes
name: build
metadata:
labels:
pod-security.kubernetes.io/audit: privileged
services:
- name: docker daemon
image: docker:dind
privileged: true
environment:
DOCKER_TLS_CERTDIR: ""
when:
event:
- tag
- promote
steps:
- name: node.js build
image: node:18
commands:
- npm add --global pnpm
- pnpm i
- rm -f ./node_modules/ngx-monaco-editor/lib/monaco.d.ts
- sed -i '1d' ./node_modules/ngx-monaco-editor/lib/types.d.ts
- ./node_modules/.bin/ionic build --prod
- ./node_modules/.bin/ngsw-config ./www/ ./ngsw-config.json /i
- echo -n $(npx uuid) | tee ./www/version.html
environment:
NODE_OPTIONS: --openssl-legacy-provider
- name: container build
image: docker:latest
privileged: true
commands:
- "while ! docker stats --no-stream; do sleep 1; done"
- docker image build -t $DOCKER_REGISTRY/noded/frontend .
- docker push $DOCKER_REGISTRY/noded/frontend
environment:
DOCKER_HOST: tcp://localhost:2375
DOCKER_REGISTRY:
from_secret: DOCKER_REGISTRY
when:
event:
- tag
- promote
- name: k8s rollout
image: bitnami/kubectl:latest
commands:
- kubectl rollout restart -n noded deployment/noded-frontend
when:
event:
- tag
- promote

@ -1 +0,0 @@
shamefully-hoist=true

@ -1,3 +0,0 @@
FROM joseluisq/static-web-server:2
COPY ./www /public/i

@ -1,12 +0,0 @@
# Requirements for frontend development
- Node.js 14.x
- The PNPM package manager
- The Ionic CLI, globally
- `npm --global add @ionic/cli`
## For development:
```sh
pnpm i
ionic serve
```

91
android/.gitignore vendored

@ -1,91 +0,0 @@
# NPM renames .gitignore to .npmignore
# In order to prevent that, we remove the initial "."
# And the CLI then renames it
# Using Android gitignore template: https://github.com/github/gitignore/blob/master/Android.gitignore
# Built application files
*.apk
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
</component>
</project>

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

@ -1,2 +0,0 @@
/build/*
!/build/.npmkeep

@ -1,46 +0,0 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "io.ionic.starter"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.warn("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

@ -1,19 +0,0 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -1,27 +0,0 @@
package com.getcapacitor.myapp;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.ionic.starter">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name="io.ionic.starter.MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/custom_url_scheme" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Camera, Photos, input file -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Geolocation API -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.location.gps" />
<!-- Network API -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Navigator.getUserMedia -->
<!-- Video -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Audio -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
</manifest>

@ -1,14 +0,0 @@
{
"appId": "io.ionic.starter",
"appName": "frontend",
"bundledWebRuntime": false,
"npmClient": "npm",
"webDir": "www",
"plugins": {
"SplashScreen": {
"launchShowDuration": 0
}
},
"cordova": {},
"linuxAndroidStudioPath": "/home/garrettmills/.local/android-studio/bin/studio.sh"
}

@ -1,21 +0,0 @@
package io.ionic.starter;
import android.os.Bundle;
import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Plugin;
import java.util.ArrayList;
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Initializes the Bridge
this.init(savedInstanceState, new ArrayList<Class<? extends Plugin>>() {{
// Additional plugins you've installed go here
// Ex: add(TotallyAwesomePlugin.class);
}});
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

@ -1,7 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">frontend</string>
<string name="title_activity_main">frontend</string>
<string name="package_name">io.ionic.starter</string>
<string name="custom_url_scheme">io.ionic.starter</string>
</resources>

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

@ -1,6 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<access origin="*" />
</widget>

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

@ -1,17 +0,0 @@
package com.getcapacitor.myapp;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

@ -1,29 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.1'
classpath 'com.google.gms:google-services:4.3.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

@ -1,3 +0,0 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

@ -1,24 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true

Binary file not shown.

@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

188
android/gradlew vendored

@ -1,188 +0,0 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

100
android/gradlew.bat vendored

@ -1,100 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

@ -1,5 +0,0 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

@ -1,17 +0,0 @@
ext {
minSdkVersion = 21
compileSdkVersion = 29
targetSdkVersion = 29
androidxAppCompatVersion = '1.1.0'
androidxCoreVersion = '1.2.0'
androidxMaterialVersion = '1.1.0-rc02'
androidxBrowserVersion = '1.2.0'
androidxLocalbroadcastmanagerVersion = '1.0.0'
androidxExifInterfaceVersion = '1.2.0'
firebaseMessagingVersion = '20.1.2'
playServicesLocationVersion = '17.0.0'
junitVersion = '4.12'
androidxJunitVersion = '1.1.1'
androidxEspressoCoreVersion = '3.2.0'
cordovaAndroidVersion = '7.0.0'
}

@ -25,41 +25,21 @@
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*.svg",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
},
{ "glob": "**/*", "input": "node_modules/ngx-monaco-editor/assets/monaco", "output": "./assets/monaco/" },
"src/manifest.webmanifest"
}
],
"styles": [
{
"input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css"
},
{
"input": "src/theme/variables.scss"
},
{
"input": "src/global.scss"
},
{
"input": "src/assets/font/fonts.css"
},
{
"input": "node_modules/katex/dist/katex.min.css"
}
],
"scripts": [
"node_modules/marked/lib/marked.js",
"node_modules/katex/dist/katex.min.js"
]
"scripts": []
},
"configurations": {
"production": {
@ -69,10 +49,6 @@
"with": "src/environments/environment.prod.ts"
}
],
"index": {
"input": "src/index.prod.html",
"output": "index.html"
},
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
@ -86,15 +62,9 @@
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
"maximumError": "5mb"
}
],
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json"
]
},
"ci": {
"progress": false
@ -141,8 +111,7 @@
"glob": "**/*",
"input": "src/assets",
"output": "/assets"
},
"src/manifest.webmanifest"
}
]
},
"configurations": {

@ -1,14 +0,0 @@
{
"appId": "io.ionic.starter",
"appName": "frontend",
"bundledWebRuntime": false,
"npmClient": "npm",
"webDir": "www",
"plugins": {
"SplashScreen": {
"launchShowDuration": 0
}
},
"cordova": {},
"linuxAndroidStudioPath": "/home/garrettmills/.local/android-studio/bin/studio.sh"
}

@ -1,30 +0,0 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
]
}

20983
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -9,60 +9,35 @@
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"build:prod": "rm -f ./node_modules/ngx-monaco-editor/lib/monaco.d.ts && sed -i '1d' ./node_modules/ngx-monaco-editor/lib/types.d.ts && ionic build --prod && ngsw-config ./www/ ./ngsw-config.json /i && echo -n $(uuidgen) | tee ./www/version.html",
"docker:build": "docker build -t ${DOCKER_REGISTRY}/noded/frontend .",
"docker:push": "docker push ${DOCKER_REGISTRY}/noded/frontend",
"postinstall": "sed -i '/^declare let MonacoEnvironment/d' node_modules/ngx-monaco-editor/lib/monaco.d.ts"
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/common": "~10.1.5",
"@angular/core": "~10.1.5",
"@angular/forms": "~10.1.5",
"@angular/platform-browser": "~10.1.5",
"@angular/platform-browser-dynamic": "~10.1.5",
"@angular/pwa": "^0.1001.7",
"@angular/router": "~10.1.5",
"@angular/service-worker": "~10.1.5",
"@ckeditor/ckeditor5-angular": "^2.0.1",
"@ckeditor/ckeditor5-build-decoupled-document": "^27.0.0",
"@convergencelabs/monaco-collab-ext": "^0.3.2",
"@fortawesome/fontawesome-free": "^5.15.1",
"@angular/common": "~8.1.2",
"@angular/core": "~8.1.2",
"@angular/forms": "~8.1.2",
"@angular/platform-browser": "~8.1.2",
"@angular/platform-browser-dynamic": "~8.1.2",
"@angular/router": "~8.1.2",
"@ionic-native/core": "^5.0.0",
"@ionic-native/splash-screen": "^5.0.0",
"@ionic-native/status-bar": "^5.0.0",
"@ionic/angular": "^5.3.5",
"@ionic/cli": "^6.12.0",
"@ng-stack/contenteditable": "^1.1.0",
"ag-grid-angular": "^24.1.0",
"ag-grid-community": "^24.1.0",
"angular-resize-event": "^2.0.1",
"@ionic/angular": "^4.7.1",
"angular-tree-component": "^8.5.2",
"core-js": "^2.5.4",
"dexie": "^3.0.2",
"highlight.js": "^10.6.0",
"ionic-selectable": "^4.7.1",
"katex": "0.12.0",
"marked": "1.2.5",
"moment": "^2.24.0",
"ng-connection-service": "^1.0.4",
"ngx-highlightjs": "^4.1.3",
"ngx-markdown": "^10.1.1",
"ngx-monaco-editor": "^9.0.0",
"rxjs": "~6.6.3",
"rxjs": "~6.5.1",
"tslib": "^1.9.0",
"uuid": "^3.4.0",
"zone.js": "~0.10.3"
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-devkit/architect": "~0.801.2",
"@angular-devkit/build-angular": "~0.1001.6",
"@angular-devkit/core": "~10.1.6",
"@angular-devkit/schematics": "^10.1.6",
"@angular/cli": "^10.1.6",
"@angular/compiler": "~10.1.5",
"@angular/compiler-cli": "~10.1.5",
"@angular/language-service": "~10.1.5",
"@angular-devkit/build-angular": "~0.801.2",
"@angular-devkit/core": "~8.1.2",
"@angular-devkit/schematics": "~8.1.2",
"@angular/cli": "~8.1.2",
"@angular/compiler": "~8.1.2",
"@angular/compiler-cli": "~8.1.2",
"@angular/language-service": "~8.1.2",
"@ionic/angular-toolkit": "^2.1.1",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
@ -78,7 +53,7 @@
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~4.0.3"
"typescript": "~3.4.3"
},
"description": "An Ionic project"
}

File diff suppressed because it is too large Load Diff

@ -2,8 +2,6 @@
"/link_api": {
"target": "http://localhost:8000",
"secure": false,
"ws": true,
"changeOrigin": true,
"logLevel": "debug",
"pathRewrite": {"^/link_api": ""}
}

@ -1,8 +1,5 @@
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import {LoginPage} from './pages/login/login.page';
import {AuthService} from './service/auth.service';
import {GuestOnlyGuard} from './service/guard/GuestOnly.guard';
const routes: Routes = [
{
@ -12,17 +9,15 @@ const routes: Routes = [
},
{
path: 'home',
canActivate: [AuthService],
loadChildren: () => import('./home/home.module').then(m => m.HomePageModule)
},
{
path: 'editor',
loadChildren: () => import('./components/components.module').then( m => m.ComponentsModule)
path: 'list',
loadChildren: () => import('./list/list.module').then(m => m.ListPageModule)
},
{
path: 'login',
canActivate: [GuestOnlyGuard],
component: LoginPage,
path: 'editor',
loadChildren: () => import('./pages/editor/editor.module').then( m => m.EditorPageModule)
}
];

@ -1,56 +1,48 @@
<ion-app class="dark">
<ion-split-pane contentId="main-content" *ngIf="ready$ | async">
<ion-menu class="sidebar no-print" menuId="main-menu" contentId="main-content" content="content" type="push" side="start" *ngIf="!api.isPublicUser">
<ion-split-pane when="sm">
<ion-menu class="sidebar">
<ion-header>
<ion-toolbar color="primary">
<ion-title style="font-weight: bold; color: white;">{{ appName }}
<ion-menu-toggle menu="first" autoHide="false"></ion-menu-toggle>
</ion-title>
<ion-title>Menu</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-list-header>
<ion-buttons>
<ion-button fill="outline" [color]="refreshingMenu ? 'success' : 'light'" (click)="onMenuRefresh()">
<ion-icon color="tertiary" name="refresh"></ion-icon>
Navigate
<ion-buttons class="ion-padding-end">
<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)="onCreateClick($event)">
<ion-icon color="primary" name="add-circle"></ion-icon>&nbsp;<span class="button-text">Create</span>
<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>
<ion-button fill="outline" color="light" (click)="onDeleteClick()" [disabled]="!deleteTarget">
<ion-icon color="danger" name="trash"></ion-icon>
</ion-button>
<ion-button fill="outline" color="light" (click)="onNodeMenuClick($event)" [disabled]="!menuTarget || !menuTarget.id">
<i class="fa fa-ellipsis-v" style="color: darkgrey"></i>
</ion-button>
<ion-button fill="outline" color="light" (click)="onVirtualRootClear($event)" *ngIf="virtualRootPageId" title="Show entire tree">
<i class="fa fa-search-minus" style="color: darkgrey"></i>
</ion-button>
</ion-buttons>
</ion-list-header>
<tree-root [nodes]="nodes" [options]="options"></tree-root>
</ion-list>
</ion-header>
<ion-content>
<app-tree-root
#menuTree
[items]="nodes"
iconClassField="faIconClass"
(itemSelected)="onMenuItemClick($event)"
(itemActivated)="onMenuItemActivate($event)"
(itemRightClicked)="onMenuItemRightClick($event)"
></app-tree-root>
</ion-content>
<ion-footer>
<ion-searchbar
placeholder="Quick filter"
(ionChange)="onMenuFilterChange($event)"
></ion-searchbar>
<ion-item button lines="full" (click)="showOptions($event)">
<ion-icon name="list" slot="start"></ion-icon>
<ion-label>Menu</ion-label>
<ion-item slot="end" lines="full">
<ion-icon slot="start" name="moon"></ion-icon>
<ion-label>
Dark mode
</ion-label>
<ion-toggle (ionChange)="toggleDark()" id="themeToggle" slot="end"></ion-toggle>
</ion-item>
</ion-footer>
</ion-menu>
<ion-router-outlet id="main-content" #content main></ion-router-outlet>
<div class="ion-page" main>
<ion-header> </ion-header>
<ion-content class="ion-padding">
<ion-router-outlet id="main-content"></ion-router-outlet>
</ion-content>
</div>
</ion-split-pane>
</ion-app>
</ion-app>

@ -6,33 +6,3 @@
.button-text {
color: var(--ion-color-medium-shade);
}
.tree-node-container {
.tree-node-icon {
margin-right: 10px;
}
&.page {
.tree-node-icon {
color: var(--noded-background-note);
}
}
&.db {
.tree-node-icon {
color: var(--noded-background-db);
}
}
&.code {
.tree-node-icon {
color: var(--noded-background-code);
}
}
&.files, &.file_box {
.tree-node-icon {
color: var(--noded-background-files);
}
}
}

@ -0,0 +1,67 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';
import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
let statusBarSpy, splashScreenSpy, platformReadySpy, platformSpy;
beforeEach(async(() => {
statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']);
splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']);
platformReadySpy = Promise.resolve();
platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy });
TestBed.configureTestingModule({
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
{ provide: StatusBar, useValue: statusBarSpy },
{ provide: SplashScreen, useValue: splashScreenSpy },
{ provide: Platform, useValue: platformSpy },
],
imports: [ RouterTestingModule.withRoutes([])],
}).compileComponents();
}));
it('should create the app', async () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it('should initialize the app', async () => {
TestBed.createComponent(AppComponent);
expect(platformSpy.ready).toHaveBeenCalled();
await platformReadySpy;
expect(statusBarSpy.styleDefault).toHaveBeenCalled();
expect(splashScreenSpy.hide).toHaveBeenCalled();
});
it('should have menu labels', async () => {
const fixture = await TestBed.createComponent(AppComponent);
await fixture.detectChanges();
const app = fixture.nativeElement;
const menuItems = app.querySelectorAll('ion-label');
expect(menuItems.length).toEqual(2);
expect(menuItems[0].textContent).toContain('Home');
expect(menuItems[1].textContent).toContain('List');
});
it('should have urls', async () => {
const fixture = await TestBed.createComponent(AppComponent);
await fixture.detectChanges();
const app = fixture.nativeElement;
const menuItems = app.querySelectorAll('ion-item');
expect(menuItems.length).toEqual(2);
expect(menuItems[0].getAttribute('ng-reflect-router-link')).toEqual('/home');
expect(menuItems[1].getAttribute('ng-reflect-router-link')).toEqual('/list');
});
});

@ -1,31 +1,12 @@
import {Component, OnInit, ViewChild, HostListener} from '@angular/core';
import { Component, OnInit } from '@angular/core';
import {
AlertController,
ModalController,
Platform,
PopoverController,
LoadingController,
ToastController, NavController
} from '@ionic/angular';
import { AlertController, Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { ApiService } from './service/api.service';
import { Router } from '@angular/router';
import {BehaviorSubject, Observable} from 'rxjs';
import {OptionPickerComponent} from './components/option-picker/option-picker.component';
import {OptionMenuComponent} from './components/option-menu/option-menu.component';
import {SelectorComponent} from './components/sharing/selector/selector.component';
import {SessionService} from './service/session.service';
import {SearchComponent} from './components/search/Search.component';
import {NodeTypeIcons} from './structures/node-types';
import {NavigationService} from './service/navigation.service';
import {DatabaseService} from './service/db/database.service';
import {EditorService} from './service/editor.service';
import {debug} from './utility';
import {AuthService} from './service/auth.service';
import {OpenerService} from './service/opener.service';
import {TreeRootComponent} from './components/tree-root/tree-root.component';
import { TREE_ACTIONS } from 'angular-tree-component';
import { Observable } from 'rxjs';
@Component({
selector: 'app-root',
@ -33,371 +14,47 @@ import {TreeRootComponent} from './components/tree-root/tree-root.component';
styleUrls: ['app.component.scss']
})
export class AppComponent implements OnInit {
@ViewChild('menuTree') menuTree: TreeRootComponent;
public readonly ready$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public addChildTarget: any = false;
public deleteTarget: any = false;
public menuTarget: any = false;
public refreshingMenu = false;
public lastClickEvent?: {event: MouseEvent, item: any};
public nodes = [];
public virtualRootPageId?: string;
public typeIcons = NodeTypeIcons;
public lastClickEvent: Array<any> = [];
public get appName(): string {
return this.session.appName || 'Noded';
}
public nodes = [];
public options = {
actionMapping: {
mouse: {
dblClick: (tree, node, $event) => {
console.log({ tree, node, $event });
const id = node.data.id;
this.router.navigate(['/editor', { id }]);
},
click: (tree, node, $event) => {
console.log('click', { tree, node, $event });
TREE_ACTIONS.FOCUS(tree, node, $event);
this.addChildTarget = node;
this.deleteTarget = node;
this.lastClickEvent = [tree, node, $event];
}
}
}
};
public darkMode = false;
protected loader?: any;
protected hasSearchOpen = false;
protected versionInterval?: any;
protected showedNewVersionAlert = false;
protected showedOfflineAlert = false;
constructor(
private platform: Platform,
private splashScreen: SplashScreen,
private statusBar: StatusBar,
public readonly api: ApiService,
private api: ApiService,
protected router: Router,
protected alerts: AlertController,
protected popover: PopoverController,
protected modal: ModalController,
protected session: SessionService,
protected loading: LoadingController,
protected navService: NavigationService,
protected toasts: ToastController,
protected db: DatabaseService,
protected editor: EditorService,
protected auth: AuthService,
protected opener: OpenerService,
) { }
public onMenuItemClick(event: {event: MouseEvent, item: any}) {
this.addChildTarget = false;
this.deleteTarget = false;
this.menuTarget = false;
if ( !event.item.noChildren && (!event.item.level || event.item.level === 'manage') ) {
this.addChildTarget = event.item;
}
if ( !event.item.noDelete && (!event.item.level || event.item.level === 'manage') ) {
this.deleteTarget = event.item;
}
this.menuTarget = event.item;
this.lastClickEvent = event;
}
public onMenuItemActivate(event: {event: MouseEvent, item: any}) {
this.navigateEditorToNode(event.item);
}
public onMenuItemRightClick(event: {event: MouseEvent, item: any}) {
event.event.preventDefault();
event.event.stopPropagation();
this.addChildTarget = false;
this.deleteTarget = false;
this.menuTarget = false;
if ( !event.item.noChildren && (!event.item.level || event.item.level === 'manage') ) {
this.addChildTarget = event.item;
}
if ( !event.item.noDelete && (!event.item.level || event.item.level === 'manage') ) {
this.deleteTarget = event.item;
}
this.menuTarget = event.item;
this.lastClickEvent = event;
this.onNodeMenuClick(event.event, true);
}
public onMenuFilterChange(event) {
const filterValue = event?.detail?.value;
debug('Filtering tree:', filterValue);
this.menuTree?.filterTree(filterValue);
}
async checkNewVersion() {
if ( !this.showedNewVersionAlert && await this.session.newVersionAvailable() ) {
const toast = await this.toasts.create({
cssClass: 'compat-toast-container',
header: 'Update Available',
message: `A new version of ${this.appName} is available. Please refresh to update.`,
buttons: [
{
side: 'end',
text: 'Refresh',
handler: () => {
window.location.reload();
},
},
],
});
this.showedNewVersionAlert = true;
await toast.present();
}
}
ngOnInit() {
debug('Initializing application.');
protected alerts: AlertController
) {
this.initializeApp();
}
@HostListener('window:popstate', ['$event'])
dismissModal(event) {
const modal = this.modal.getTop();
if ( modal ) {
event.preventDefault();
event.stopPropagation();
this.modal.dismiss();
}
}
showOptions($event) {
this.popover.create({
event: $event,
component: OptionPickerComponent,
componentProps: {
toggleDark: () => this.toggleDark(),
isDark: () => this.isDark(),
showSearch: () => this.handleKeyboardEvent(),
isPrefetch: () => this.isPrefetch(),
togglePrefetch: () => this.togglePrefetch(),
doPrefetch: () => this.doPrefetch(),
}
}).then(popover => popover.present());
}
@HostListener('document:keyup.control./', ['$event'])
async handleKeyboardEvent() {
if ( this.hasSearchOpen ) {
return;
}
const modal = await this.modal.create({
component: SearchComponent,
cssClass: 'modal-med',
});
const modalState = {
modal : true,
desc : 'Search everything'
};
history.pushState(modalState, null);
this.hasSearchOpen = true;
await modal.present();
await modal.onDidDismiss();
this.hasSearchOpen = false;
}
public navigateEditorToNode(node: any) {
if ( !node.data ) {
node = { data: node };
}
const id = node.data.id;
const nodeId = node.data.node_id;
if ( !node.data.virtual ) {
debug('Navigating editor to node:', {id, nodeId});
this.opener.currentPageId = id;
this.opener.openTarget(id, nodeId);
}
}
async onNodeMenuClick($event, fromContextMenu = false) {
let canManage = this.menuTarget.level === 'manage';
if ( !canManage ) {
if ( !this.menuTarget.level ) {
canManage = true;
}
}
if ( !this.menuTarget.id ) {
return;
}
const options = [
{name: 'Make Virtual Root', icon: 'fa fa-search-plus', value: 'virtual_root'},
{name: 'Export to HTML', icon: 'fa fa-file-export', value: 'export_html'},
// {name: 'Export as PDF', icon: 'fa fa-file-export', value: 'export_pdf'},
];
const manageOptions = [
...(fromContextMenu ? this.getCreateNodeMenuItems() : []),
{name: 'Share Sub-Tree', icon: 'fa fa-share-alt', value: 'share'},
{name: 'Delete Sub-Tree', icon: 'fa fa-trash noded-danger', value: 'delete'},
];
if ( this.menuTarget.bookmark ) {
options.push({name: 'Remove Bookmark', icon: 'fa fa-star', value: 'bookmark_remove'});
} else {
options.push({name: 'Bookmark', icon: 'fa fa-star', value: 'bookmark_add'});
}
const popover = await this.popover.create({
component: OptionMenuComponent,
componentProps: {
menuItems: [
...(!canManage ? options : [...options, ...manageOptions]),
],
},
event: $event,
});
popover.onDidDismiss().then((result) => {
if ( result.data === 'share' ) {
this.modal.create({
component: SelectorComponent,
cssClass: 'modal-med',
componentProps: {
node: this.menuTarget,
}
}).then(modal => {
const modalState = {
modal : true,
desc : 'Share page'
};
history.pushState(modalState, null);
modal.present();
});
} else if ( result.data === 'export_html' ) {
this.exportTargetAsHTML();
} else if ( result.data === 'export_pdf' ) {
// this.exportTargetAsPDF();
} else if ( result.data === 'virtual_root' ) {
this.setVirtualRoot();
} else if ( result.data === 'bookmark_add' ) {
this.addBookmark();
} else if ( result.data === 'bookmark_remove' ) {
this.removeBookmark();
} else if ( result.data === 'top-level' ) {
this.onTopLevelCreate();
} else if ( result.data === 'child' ) {
this.onChildCreate();
} else if ( result.data === 'form' ) {
this.onChildCreate('form');
} else if ( result.data === 'delete' ) {
this.onDeleteClick();
}
});
await popover.present();
}
async setVirtualRoot() {
if ( this.menuTarget && this.menuTarget?.type === 'page' ) {
debug('virtual root menu target', this.menuTarget);
this.virtualRootPageId = this.menuTarget.id;
this.reloadMenuItems().subscribe();
}
}
onVirtualRootClear(event) {
delete this.virtualRootPageId;
ngOnInit() {
this.reloadMenuItems().subscribe();
}
async exportTargetAsHTML() {
const exportRecord: any = await new Promise((res, rej) => {
const reqData = {
format: 'html',
PageId: this.menuTarget.id,
};
this.api.post(`/exports/subtree`, reqData).subscribe({
next: (result) => {
res(result.data);
},
error: rej
});
});
const dlUrl = this.api._build_url(`/exports/${exportRecord.UUID}/download`);
window.open(dlUrl, '_blank');
}
addBookmark() {
const bookmarks = this.session.get('user.preferences.bookmark_page_ids') || [];
if ( !bookmarks.includes(this.menuTarget.id) ) {
bookmarks.push(this.menuTarget.id);
}
this.session.set('user.preferences.bookmark_page_ids', bookmarks);
this.session.save().then(() => this.navService.requestSidebarRefresh({ quiet: true }));
}
removeBookmark() {
let bookmarks = this.session.get('user.preferences.bookmark_page_ids') || [];
bookmarks = bookmarks.filter(x => x !== this.menuTarget.id);
this.session.set('user.preferences.bookmark_page_ids', bookmarks);
this.session.save().then(() => this.navService.requestSidebarRefresh({ quiet: true }));
}
getCreateNodeMenuItems() {
return [
{
name: 'Create Top-Level Note',
icon: 'fa fa-sticky-note noded-note',
value: 'top-level',
title: 'Create a new top-level note page',
},
...(this.addChildTarget ? [
{
name: 'Create Child Note',
icon: 'fa fa-sticky-note noded-note',
value: 'child',
title: 'Create a note page as a child of the given note',
},
{
name: 'Create Form',
icon: 'fa fa-clipboard-list noded-form',
value: 'form',
title: 'Create a new form page as a child of the given note',
},
] : []),
];
}
async onCreateClick($event: MouseEvent) {
const menuItems = this.getCreateNodeMenuItems();
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',
@ -419,9 +76,10 @@ export class AppComponent implements OnInit {
{
text: 'Create',
handler: async args => {
const page = await this.editor.createPage(args.name);
this.reloadMenuItems().subscribe();
await this.router.navigate(['/editor', { id: page.UUID }]);
this.api.post('/page/create', args).subscribe(res => {
this.router.navigate(['/editor', { id: res.data.UUID }]);
this.reloadMenuItems().subscribe();
});
}
}
]
@ -430,7 +88,7 @@ export class AppComponent implements OnInit {
await alert.present();
}
async onChildCreate(pageType?: string) {
async onChildCreate() {
const alert = await this.alerts.create({
header: 'Create Sub-Page',
message: 'Please enter a new name for the page:',
@ -453,11 +111,15 @@ export class AppComponent implements OnInit {
handler: async args => {
args = {
name: args.name,
parentId: this.addChildTarget.id,
pageType,
parentId: this.addChildTarget.data.id
};
this.api.post('/page/create-child', args).subscribe(res => {
this.reloadMenuItems().subscribe(() => {
TREE_ACTIONS.EXPAND(
this.lastClickEvent[0],
this.lastClickEvent[1],
this.lastClickEvent[2]
);
this.router.navigate(['/editor', { id: res.data.UUID }]);
});
});
@ -473,7 +135,7 @@ export class AppComponent implements OnInit {
const alert = await this.alerts.create({
header: 'Delete page?',
message:
'Deleting this page will make its contents and all of its children inaccessible. Are you sure you want to continue?',
'Deleting this page will make its contents and all of its children inaccessible. Are you sure you want to continue?',
buttons: [
{
text: 'Keep It',
@ -483,16 +145,11 @@ export class AppComponent implements OnInit {
text: 'Delete It',
handler: async () => {
this.api
.post(`/page/delete/${this.deleteTarget.id}`)
.post(`/page/delete/${this.deleteTarget.data.id}`)
.subscribe(res => {
if ( this.opener.currentPageId === this.deleteTarget.id ) {
this.router.navigate(['/home']);
}
this.reloadMenuItems().subscribe();
this.deleteTarget = false;
this.addChildTarget = false;
this.menuTarget = false;
});
}
}
@ -502,321 +159,27 @@ export class AppComponent implements OnInit {
await alert.present();
}
onMenuRefresh(quiet = false) {
if ( !quiet ) {
this.refreshingMenu = true;
}
this.reloadMenuItems().subscribe();
setTimeout(() => {
if ( !quiet ) {
this.refreshingMenu = false;
}
}, 2000);
}
reloadMenuItems() {
return new Observable(sub => {
this.api.getMenuItems(false, this.virtualRootPageId).then(nodes => {
this.nodes = nodes;
this.api.get('/menu/items').subscribe(result => {
this.nodes = result.data;
sub.next();
sub.complete();
});
});
}
async initializeApp() {
const initializedOnce = this.navService.initialized$.getValue();
if ( this.isDark() ) {
this.toggleDark();
}
debug('app', this);
this.loader = await this.loading.create({
message: 'Setting things up...',
cssClass: 'noded-loading-mask',
showBackdrop: true,
});
debug('Initializing platform and database...');
await this.loader.present();
await this.platform.ready();
await this.db.createSchemata();
let toast: any;
if ( !initializedOnce ) {
debug('Subscribing to offline changes...');
this.api.offline$.subscribe(async isOffline => {
if ( isOffline && !this.showedOfflineAlert ) {
debug('Application went offline!');
toast = await this.toasts.create({
cssClass: 'compat-toast-container',
message: 'Uh, oh! It looks like you\'re offline. Some features might not work as expected...',
});
this.showedOfflineAlert = true;
await toast.present();
} else if ( !isOffline && this.showedOfflineAlert ) {
debug('Appliation went online!');
await toast.dismiss();
this.showedOfflineAlert = false;
await this.api.syncOfflineData();
}
});
}
debug('Getting initial status...');
let stat: any = await this.session.stat();
debug('Got stat:', stat);
this.api.isPublicUser = !!stat.public_user;
this.api.isAuthenticated = !!stat.authenticated_user;
this.api.systemBase = stat.system_base;
if ( !this.api.isAuthenticated || this.api.isPublicUser ) {
debug('Unauthenticated or public user...');
if ( !this.api.isOffline ) {
debug('Trying to resume session...');
await this.api.resumeSession();
debug('Checking new status...');
stat = await this.session.stat();
debug('Got session resume stat:', stat);
this.api.isAuthenticated = stat.authenticated_user;
this.api.isPublicUser = stat.public_user;
if ( !stat.authenticated_user ) {
debug('Not authenticated! Redirecting.');
window.location.href = `${stat.system_base}start`;
return;
}
} else {
debug('Unauthenticated offline user. Purging local data!');
await this.db.purge();
window.location.href = `${stat.system_base}start`;
return;
}
}
debug('Set app name and system base:', stat.app_name, stat.system_base);
this.session.appName = stat.app_name;
this.session.systemBase = stat.system_base;
debug('Initializing session...');
await this.session.initialize();
if ( this.session.get('user.preferences.dark_mode') && !this.darkMode ) {
this.toggleDark();
}
debug('Hiding native splash screen & setting status bar styles...');
await this.statusBar.styleDefault();
await this.splashScreen.hide();
// If we went online after being offline, sync the local data
if ( !this.api.isOffline && await this.api.needsSync() ) {
this.loader.message = 'Syncing data...';
try {
await this.api.syncOfflineData();
} catch (e) {
this.toasts.create({
cssClass: 'compat-toast-container',
message: 'An error occurred while syncing offline data. Not all data was saved.',
buttons: [
'Okay'
],
}).then(tst => {
tst.present();
});
}
}
if ( this.isPrefetch() && !this.api.isPublicUser ) {
debug('Pre-fetching offline data...');
this.loader.message = 'Downloading data...';
try {
await this.api.prefetchOfflineData();
} catch (e) {
debug('Pre-fetch error:', e);
this.toasts.create({
cssClass: 'compat-toast-container',
message: 'An error occurred while pre-fetching offline data. Not all data was saved.',
buttons: [
'Okay'
],
}).then(tst => {
tst.present();
});
}
}
this.navService.initialized$.next(true);
if ( !this.api.isPublicUser && this.session.get('user.preferences.default_page') ) {
debug('Navigating to default page!');
const id = this.session.get('user.preferences.default_page');
const node = this.findNode(id);
if ( node ) {
this.navigateEditorToNode(node);
} else if ( this.auth.authInProgress ) {
await this.router.navigate(['/home']);
}
} else if ( this.auth.authInProgress ) {
await this.router.navigate(['/home']);
}
if ( !initializedOnce ) {
debug('Creating menu subscription...');
this.navService.sidebarRefresh$.subscribe(([_, quiet]) => {
this.onMenuRefresh(quiet);
});
this.navService.navigationRequest$.subscribe(request => {
debug('Page navigation request: ', request);
if ( !request.pageId ) {
debug('Empty page ID. Will not navigate.');
return;
}
this.opener.currentPageId = request.pageId;
this.router.navigate(['/editor', {
id: request.pageId,
...(request.nodeId ? { node_id: request.nodeId } : {}),
}]);
});
this.navService.initializationRequest$.subscribe((count) => {
if ( count === 0 ) {
return;
}
this.initializeApp().then(() => {
this.router.navigate(['/login']);
});
});
}
debug('Reloading menu items...');
this.reloadMenuItems().subscribe(() => {
debug('Reloaded menu items. Displaying interface.');
this.ready$.next(true);
setTimeout(() => {
this.loader.dismiss();
// this.menuTree?.treeModel?.expandAll();
}, 10);
if ( !this.versionInterval ) {
this.versionInterval = setInterval(() => {
debug('Checking for new application version.');
this.checkNewVersion();
}, 1000 * 60 * 5); // Check for new version every 5 mins
}
});
this.auth.authInProgress = false;
}
async doPrefetch() {
if ( this.api.isOffline ) {
return;
}
this.loader = await this.loading.create({
message: 'Pre-fetching data...',
cssClass: 'noded-loading-mask',
showBackdrop: true,
initializeApp() {
this.platform.ready().then(() => {
this.statusBar.styleDefault();
this.splashScreen.hide();
});
await new Promise(res => setTimeout(res, 2000));
await this.loader.present();
try {
if (await this.api.needsSync()) {
this.loader.message = 'Syncing data...';
await this.api.syncOfflineData();
}
this.loader.message = 'Downloading data...';
await this.api.prefetchOfflineData();
} catch (e) {
const msg = await this.alerts.create({
header: 'Uh, oh!',
message: 'An unexpected error occurred while trying to sync offline data, and we were unable to continue.',
buttons: [
'OK',
],
});
await msg.present();
}
this.loader.dismiss();
}
toggleDark() {
// const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
this.darkMode = !this.darkMode;
this.session.set('user.preferences.dark_mode', this.darkMode);
console.log('toggel Dark mode');
document.body.classList.toggle('dark', this.darkMode);
}
togglePrefetch() {
this.session.set('user.preferences.auto_prefetch', !this.isPrefetch());
}
findNode(id: string, nodes = this.nodes) {
for ( const node of nodes ) {
if ( node.id === id ) {
return node;
}
if ( node.children ) {
const foundNode = this.findNode(id, node.children);
if ( foundNode ) {
return foundNode;
}
}
}
}
isDark() {
return !!this.darkMode;
}
isPrefetch() {
return !!this.session.get('user.preferences.auto_prefetch');
}
async onTreeNodeMove({ node, to }) {
if ( this.api.isOffline ) {
debug('Cannot move node. API is offline.');
return;
}
const { parent } = to;
debug('Moving node:', { node, parent });
try {
await this.api.moveMenuNode(node.id, to.parent.id);
} catch (error) {
console.error('Error moving tree node:', error);
this.alerts.create({
header: 'Error Moving Node',
message: error.message,
buttons: [
{
text: 'Okay',
role: 'cancel',
},
],
}).then(x => x.present());
}
await this.reloadMenuItems().toPromise();
}
}

@ -1,41 +1,16 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { RouteReuseStrategy } from "@angular/router";
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { IonicModule, IonicRouteStrategy } from "@ionic/angular";
import { SplashScreen } from "@ionic-native/splash-screen/ngx";
import { StatusBar } from "@ionic-native/status-bar/ngx";
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';
import { ComponentsModule } from './components/components.module';
import {AgGridModule} from 'ag-grid-angular';
import {MonacoEditorModule} from 'ngx-monaco-editor';
import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
import {MarkdownModule, MarkedOptions} from 'ngx-markdown';
import {ConnectionServiceModule} from 'ng-connection-service';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { AngularResizedEventModule } from 'angular-resize-event';
import { IonicSelectableModule } from 'ionic-selectable';
import * as hljs from 'highlight.js';
import { HighlightModule, HIGHLIGHT_OPTIONS } from 'ngx-highlightjs';
/**
* This function is used internal to get a string instance of the `<base href="" />` value from `index.html`.
* This is an exported function, instead of a private function or inline lambda, to prevent this error:
*
* `Error encountered resolving symbol values statically.`
* `Function calls are not supported.`
* `Consider replacing the function or lambda with a reference to an exported function.`
*
* @param platformLocation an Angular service used to interact with a browser's URL
* @return a string instance of the `<base href="" />` value from `index.html`
*/
export function getBaseHref(platformLocation: PlatformLocation): string {
return platformLocation.getBaseHrefFromDOM();
}
import { AppComponent } from "./app.component";
import { AppRoutingModule } from "./app-routing.module";
import { HttpClientModule } from "@angular/common/http";
import { ComponentsModule } from "./components/components.module";
import { TreeModule } from "angular-tree-component";
@NgModule({
declarations: [AppComponent],
@ -46,45 +21,12 @@ export function getBaseHref(platformLocation: PlatformLocation): string {
AppRoutingModule,
HttpClientModule,
ComponentsModule,
AgGridModule.withComponents([]),
MonacoEditorModule.forRoot(),
HighlightModule,
MarkdownModule.forRoot({
markedOptions: {
provide: MarkedOptions,
useValue: {
highlight(code: string, lang: string, callback?: (error: any, code: string) => void): string {
const highlighted = hljs.highlight(lang, code, true);
if ( callback ) {
callback(null, highlighted.value);
}
return highlighted.value;
},
},
}
}),
ConnectionServiceModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
AngularResizedEventModule,
IonicSelectableModule,
TreeModule.forRoot()
],
providers: [
StatusBar,
SplashScreen,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{
provide: APP_BASE_HREF,
useFactory: getBaseHref,
deps: [PlatformLocation]
},
{
provide: HIGHLIGHT_OPTIONS,
useValue: {
fullLibraryLoader: () => import('highlight.js'),
}
},
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
],
bootstrap: [AppComponent]
})

@ -1,202 +1,11 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {NodePickerComponent} from './editor/node-picker/node-picker.component';
import {IonicModule} from '@ionic/angular';
import {DatabaseComponent} from './editor/database/database.component';
import {AgGridModule} from 'ag-grid-angular';
import {ColumnsComponent} from './editor/database/columns/columns.component';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {ContenteditableModule} from '@ng-stack/contenteditable';
import {CodeComponent} from './editor/code/code.component';
import {MonacoEditorModule} from 'ngx-monaco-editor';
import {IonicSelectableModule} from 'ionic-selectable';
import {FilesComponent} from './editor/files/files.component';
import {OptionPickerComponent} from './option-picker/option-picker.component';
import {HostOptionsComponent} from './editor/host-options/host-options.component';
import {OptionMenuComponent} from './option-menu/option-menu.component';
import {SelectorComponent} from './sharing/selector/selector.component';
import {NumericEditorComponent} from './editor/database/editors/numeric/numeric-editor.component';
import {ParagraphEditorComponent} from './editor/database/editors/paragraph/paragraph-editor.component';
import {ParagraphModalComponent} from './editor/database/editors/paragraph/paragraph-modal.component';
import {BooleanEditorComponent} from './editor/database/editors/boolean/boolean-editor.component';
import {SelectEditorComponent} from './editor/database/editors/select/select-editor.component';
import {MultiSelectEditorComponent} from './editor/database/editors/select/multiselect-editor.component';
import {DatetimeEditorComponent} from './editor/database/editors/datetime/datetime-editor.component';
import {DatetimeRendererComponent} from './editor/database/renderers/datetime-renderer.component';
import {CurrencyRendererComponent} from './editor/database/renderers/currency-renderer.component';
import {BooleanRendererComponent} from './editor/database/renderers/boolean-renderer.component';
import {SearchComponent} from './search/Search.component';
import {TreeRootComponent} from './tree-root/tree-root.component';
import {NormComponent} from './nodes/norm/norm.component';
import {MarkdownComponent as MarkdownEditorComponent} from './nodes/markdown/markdown.component';
import {DirectivesModule} from '../directives/directives.module';
import {MarkdownModule} from 'ngx-markdown';
import {VersionModalComponent} from './version-modal/version-modal.component';
import {EditorPageRoutingModule} from '../pages/editor/editor-routing.module';
import {ViewerPageRoutingModule} from '../pages/viewer/viewer-routing.module';
import {EditorPage} from '../pages/editor/editor.page';
import {ViewerPage} from '../pages/viewer/viewer.page';
import {LoginPage} from '../pages/login/login.page';
import {WysiwygComponent} from './wysiwyg/wysiwyg.component';
import {WysiwygEditorComponent} from './editor/database/editors/wysiwyg/wysiwyg-editor.component';
import {WysiwygModalComponent} from './editor/database/editors/wysiwyg/wysiwyg-modal.component';
import {AngularResizedEventModule} from 'angular-resize-event';
import {DateTimeFilterComponent} from './editor/database/filters/date-time.filter';
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 {LinkRendererComponent} from './editor/database/renderers/link-renderer.component';
import {FormInputComponent} from './nodes/form-input/form-input.component';
import {FormInputOptionsComponent} from './nodes/form-input/options/form-input-options.component';
import {DatabaseLinkComponent} from './editor/forms/database-link.component';
import {FileBoxComponent} from './nodes/file-box/file-box.component';
import {FileBoxPageComponent} from './nodes/file-box/file-box-page.component';
import { HostComponent } from './editor/host/host.component';
@NgModule({
declarations: [
NodePickerComponent,
DatabaseComponent,
ColumnsComponent,
CodeComponent,
FilesComponent,
OptionPickerComponent,
HostOptionsComponent,
OptionMenuComponent,
SelectorComponent,
NumericEditorComponent,
ParagraphEditorComponent,
ParagraphModalComponent,
BooleanEditorComponent,
SelectEditorComponent,
MultiSelectEditorComponent,
DatetimeEditorComponent,
DatetimeRendererComponent,
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
TreeRootComponent,
NormComponent,
MarkdownEditorComponent,
VersionModalComponent,
EditorPage,
ViewerPage,
LoginPage,
WysiwygComponent,
WysiwygEditorComponent,
WysiwygModalComponent,
DateTimeFilterComponent,
DatabasePageComponent,
PageLinkRendererComponent,
LinkRendererComponent,
PageLinkEditorComponent,
FormInputComponent,
FormInputOptionsComponent,
DatabaseLinkComponent,
FileBoxComponent,
FileBoxPageComponent,
],
imports: [
CommonModule,
IonicModule,
AgGridModule,
FormsModule,
ReactiveFormsModule,
ContenteditableModule,
MonacoEditorModule,
DirectivesModule,
MarkdownModule,
EditorPageRoutingModule,
ViewerPageRoutingModule,
AngularResizedEventModule,
IonicSelectableModule,
],
entryComponents: [
NodePickerComponent,
DatabaseComponent,
ColumnsComponent,
CodeComponent,
FilesComponent,
OptionPickerComponent,
HostOptionsComponent,
OptionMenuComponent,
SelectorComponent,
NumericEditorComponent,
ParagraphEditorComponent,
ParagraphModalComponent,
BooleanEditorComponent,
SelectEditorComponent,
MultiSelectEditorComponent,
DatetimeEditorComponent,
DatetimeRendererComponent,
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
TreeRootComponent,
NormComponent,
MarkdownEditorComponent,
VersionModalComponent,
EditorPage,
ViewerPage,
LoginPage,
WysiwygComponent,
WysiwygEditorComponent,
WysiwygModalComponent,
DateTimeFilterComponent,
DatabasePageComponent,
PageLinkRendererComponent,
LinkRendererComponent,
PageLinkEditorComponent,
FormInputComponent,
FormInputOptionsComponent,
DatabaseLinkComponent,
FileBoxComponent,
FileBoxPageComponent,
],
exports: [
NodePickerComponent,
DatabaseComponent,
ColumnsComponent,
CodeComponent,
FilesComponent,
OptionPickerComponent,
HostOptionsComponent,
OptionMenuComponent,
SelectorComponent,
NumericEditorComponent,
ParagraphEditorComponent,
ParagraphModalComponent,
BooleanEditorComponent,
SelectEditorComponent,
MultiSelectEditorComponent,
DatetimeEditorComponent,
DatetimeRendererComponent,
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
TreeRootComponent,
NormComponent,
MarkdownEditorComponent,
VersionModalComponent,
EditorPage,
ViewerPage,
LoginPage,
WysiwygComponent,
WysiwygEditorComponent,
WysiwygModalComponent,
DateTimeFilterComponent,
DatabasePageComponent,
PageLinkRendererComponent,
LinkRendererComponent,
PageLinkEditorComponent,
FormInputComponent,
FormInputOptionsComponent,
DatabaseLinkComponent,
FileBoxComponent,
FileBoxPageComponent,
]
declarations: [HostComponent],
imports: [CommonModule],
entryComponents: [HostComponent],
exports: [HostComponent]
})
export class ComponentsModule {}

@ -1,23 +0,0 @@
<div class="code-wrapper" style="width: 100%; margin-top: 10px;" *ngIf="!notAvailableOffline">
<ion-toolbar>
<ion-item>
<ion-label position="floating">Language</ion-label>
<ion-select style="min-width: 40px;" [(ngModel)]="editorOptions.language" (ionChange)="onSelectChange()" [disabled]="readonly">
<ion-select-option *ngFor="let lang of languageOptions" [value]="lang.toLowerCase()">{{lang}}</ion-select-option>
</ion-select>
</ion-item>
</ion-toolbar>
<div class="editor-container" #editorContainer>
<ngx-monaco-editor style="width: 100%; height: 100%;"
[options]="editorOptions"
[(ngModel)]="editorValue"
(onInit)="onMonacoEditorInit($event)"
(ngModelChange)="onEditorModelChange($event)"
#theEditor
class="editor"
></ngx-monaco-editor>
</div>
</div>
<div class="code-wrapper not-offline" style="width: 100%; height: 600px; margin-top: 10px;" *ngIf="notAvailableOffline">
Sorry, this code editor is not available offline yet.
</div>

@ -1,10 +0,0 @@
div.code-wrapper {
border: 2px solid #8c8c8c;
border-radius: 3px;
&.not-offline {
text-align: center;
padding-top: 100px;
color: #595959;
}
}

@ -1,470 +0,0 @@
import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {v4} from 'uuid';
import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service';
import {EditorNodeContract} from '../../nodes/EditorNode.contract';
import {EditorService} from '../../../service/editor.service';
import {EditorComponent} from 'ngx-monaco-editor';
import * as MonacoCollabExt from '@convergencelabs/monaco-collab-ext';
import {FlitterSocketConnection, FlitterSocketServerClientTransaction} from '../../../flitter-socket';
import {debug} from '../../../utility';
import {environment} from '../../../../environments/environment';
export interface RemoteUser {
uuid: string;
uid: string;
display: string;
color: string;
cursor?: any;
selection?: any;
}
export interface RemoteInsertOperation {
type: 'insert';
index: number;
text: string;
fullContents?: string;
}
export interface RemoteReplaceOperation {
type: 'replace';
index: number;
text: string;
length: number;
fullContents?: string;
}
export interface RemoteDeleteOperation {
type: 'delete';
index: number;
length: number;
fullContents?: string;
}
export type RemoteOperation = RemoteInsertOperation | RemoteReplaceOperation | RemoteDeleteOperation;
@Component({
selector: 'editor-code',
templateUrl: './code.component.html',
styleUrls: ['./code.component.scss'],
})
export class CodeComponent extends EditorNodeContract implements OnInit, OnDestroy {
@Input() nodeId: string;
@Input() editorUUID?: string;
@ViewChild('theEditor') theEditor: EditorComponent;
@ViewChild('editorContainer') editorContainer: ElementRef;
public dirty = false;
protected dbRecord: any = {};
protected codeRefId!: string;
public notAvailableOffline = false;
public containerHeight = 540;
protected cursorManager?: MonacoCollabExt.RemoteCursorManager;
protected selectionManager?: MonacoCollabExt.RemoteSelectionManager;
protected contentManager?: MonacoCollabExt.EditorContentManager;
protected remoteUsers: RemoteUser[] = [];
protected localUser?: RemoteUser;
protected socket?: FlitterSocketConnection;
protected editorGroupID!: string;
public editorOptions = {
theme: this.isDark() ? 'vs-dark' : 'vs',
language: 'javascript',
uri: v4(),
readOnly: false,
automaticLayout: true,
scrollBeyondLastLine: false,
scrollbar: {
alwaysConsumeMouseWheel: false,
},
};
public editorValue = '';
public get readonly() {
return !this.node || !this.editorService.canEdit();
}
public languageOptions: Array<string> = [
'ABAP',
'AES',
'Apex',
'AZCLI',
'Bat',
'C',
'Cameligo',
'Clojure',
'CoffeeScript',
'Cpp',
'Csharp',
'CSP',
'CSS',
'Dockerfile',
'Fsharp',
'Go',
'GraphQL',
'Handlebars',
'HTML',
'INI',
'Java',
'JavaScript',
'JSON',
'Kotlin',
'LeSS',
'Lua',
'Markdown',
'MiPS',
'MSDAX',
'MySQL',
'Objective-C',
'Pascal',
'Pascaligo',
'Perl',
'pgSQL',
'PHP',
'Plaintext',
'Postiats',
'PowerQuery',
'PowerShell',
'Pug',
'Python',
'R',
'Razor',
'Redis',
'RedShift',
'RestructuredText',
'Ruby',
'Rust',
'SB',
'Scheme',
'SCSS',
'Shell',
'SOL',
'SQL',
'St',
'Swift',
'TCL',
'Twig',
'TypeScript',
'VB',
'XML',
'YAML',
];
protected hadLoad = false;
constructor(
public editorService: EditorService,
public readonly api: ApiService,
) { super(); }
public isDark() {
return document.body.classList.contains('dark');
}
public isDirty(): boolean | Promise<boolean> {
return this.dirty;
}
public needsSave(): boolean | Promise<boolean> {
return this.dirty;
}
public writeChangesToNode(): void | Promise<void> {
this.node.Value.Mode = 'code';
this.node.Value.Value = this.codeRefId;
this.node.value = this.codeRefId;
}
public needsLoad(): boolean | Promise<boolean> {
return this.node && !this.hadLoad;
}
public performLoad(): void | Promise<void> {
return new Promise((res, rej) => {
if ( !this.node.Value ) {
this.node.Value = {};
}
if ( !this.node.Value.Value && this.editorService.canEdit() ) {
this.api.createCodium(this.page.UUID, this.node.UUID).then(data => {
this.dbRecord = data;
this.node.Value.Mode = 'code';
this.node.Value.Value = data.UUID;
this.node.value = data.UUID;
this.codeRefId = data.UUID;
this.editorOptions.readOnly = this.readonly;
this.onSelectChange(false);
this.hadLoad = true;
this.notAvailableOffline = false;
res();
}).catch(rej);
} else {
this.api.getCodium(this.page.UUID, this.node.UUID, this.node.Value.Value, this.node.associatedTypeVersionNum).then(data => {
this.dbRecord = data;
this.initialValue = this.dbRecord.code;
this.editorValue = this.dbRecord.code;
this.editorOptions.language = this.dbRecord.Language;
this.codeRefId = this.node.Value.Value;
this.editorOptions.readOnly = this.readonly;
this.onSelectChange(false);
this.hadLoad = true;
this.notAvailableOffline = false;
res();
}).catch(e => {
if ( e instanceof ResourceNotAvailableOfflineError ) {
this.notAvailableOffline = true;
} else {
rej(e);
}
});
}
});
}
public performSave(): void | Promise<void> {
if ( !this.editorService.canEdit() ) {
return;
}
return new Promise((res, rej) => {
this.dbRecord.code = this.editorValue;
this.dbRecord.Language = this.editorOptions.language;
this.api.saveCodium(this.page.UUID, this.node.UUID, this.node.Value.Value, this.dbRecord).then(data => {
this.dbRecord = data;
this.editorOptions.language = this.dbRecord.Language;
this.editorValue = this.dbRecord.code;
this.dirty = false;
res();
}).catch(rej);
});
}
public performDelete(): void | Promise<void> {
return this.api.deleteCodium(this.page.UUID, this.node.UUID, this.node.Value.Value);
}
ngOnInit() {
this.editorService = this.editorService.getEditor(this.editorUUID);
this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
this.editorOptions.readOnly = !this.editorService.canEdit();
});
const url = `${environment.websocketBase}/api/v1/socket/code/.websocket`;
debug(`Editor socket URL: ${url}`);
if ( !this.editorService.isVersion() ) {
const socket = new FlitterSocketConnection(url);
socket.controller(this);
socket.on_open().then(() => {
debug('Connected to code editor socket', socket);
socket.asyncRequest('subscribe', { resource_id: this.node.Value.Value }).then(([transaction, _, data]) => {
debug('Subscribed to editor group:', data);
if ( data.editor_group_id ) {
this.editorGroupID = data.editor_group_id;
this.socket = socket;
this.localUser = data.local_user;
}
});
});
}
}
ngOnDestroy() {
if ( this.socket ) {
this.socket.socket.close();
}
}
onMonacoEditorInit(editor) {
let ignoreEvent = false;
const updateHeight = () => {
const contentHeight = Math.max(540, editor.getContentHeight());
this.containerHeight = contentHeight;
try {
ignoreEvent = true;
editor.layout({ width: this.editorContainer.nativeElement.offsetWidth, height: contentHeight });
} finally {
ignoreEvent = false;
}
};
editor.onDidChangeCursorPosition(event => {
this.socket?.asyncRequest('update_cursor', {
position: event.position,
uuid: this.localUser?.uuid,
editor_group_id: this.editorGroupID,
});
});
editor.onDidChangeCursorSelection(event => {
this.socket?.asyncRequest('update_selection', {
startPosition: {
lineNumber: event.selection.startLineNumber,
column: event.selection.startColumn,
},
endPosition: {
lineNumber: event.selection.endLineNumber,
column: event.selection.endColumn,
},
uuid: this.localUser?.uuid,
editor_group_id: this.editorGroupID,
});
});
editor.onDidContentSizeChange(updateHeight);
updateHeight();
this.cursorManager = new MonacoCollabExt.RemoteCursorManager({
editor,
tooltips: true,
tooltipDuration: 2,
});
this.selectionManager = new MonacoCollabExt.RemoteSelectionManager({ editor });
this.contentManager = new MonacoCollabExt.EditorContentManager({
editor,
onInsert: (index, text) => {
if ( this.readonly ) {
return;
}
this.socket?.asyncRequest('apply', {
editor_group_id: this.editorGroupID,
operations: [
{
type: 'insert',
index,
text,
},
],
});
},
onReplace: (index, length, text) => {
if ( this.readonly ) {
return;
}
this.socket?.asyncRequest('apply', {
editor_group_id: this.editorGroupID,
operations: [
{
type: 'replace',
index,
text,
length,
},
],
});
},
onDelete: (index, length) => {
if ( this.readonly ) {
return;
}
this.socket?.asyncRequest('apply', {
editor_group_id: this.editorGroupID,
operations: [
{
type: 'delete',
index,
length,
},
],
});
},
});
}
applyOperation(op: RemoteOperation) {
if ( op.type === 'insert' ) {
this.contentManager?.insert(op.index, op.text);
} else if ( op.type === 'replace' ) {
this.contentManager?.replace(op.index, op.length, op.text);
} else if ( op.type === 'delete' ) {
this.contentManager?.delete(op.index, op.length);
}
}
async applyRemoteOperation(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
const ops: RemoteOperation[] = transaction.incoming.operations || [];
for ( const op of ops ) {
this.applyOperation(op);
}
}
async updateCursorPosition(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
const position = transaction.incoming.position;
const uuid = transaction.incoming.uuid;
for ( const user of this.remoteUsers ) {
if ( user.uuid === uuid ) {
user.cursor?.setPosition(position);
}
}
}
async updateSelection(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
const startPosition = transaction.incoming.startPosition;
const endPosition = transaction.incoming.endPosition;
const uuid = transaction.incoming.uuid;
for ( const user of this.remoteUsers ) {
if ( user.uuid === uuid ) {
if ( startPosition && endPosition ) {
user.selection?.setPositions(startPosition, endPosition);
user.selection?.show();
} else {
user.selection?.hide();
}
}
}
}
async setEditorGroupUsers(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
const remoteUsers: RemoteUser[] = Array.isArray(transaction.incoming?.users) ? transaction.incoming.users : [];
console.log('set editor group users', remoteUsers, transaction);
for ( const user of this.remoteUsers ) {
this.cursorManager?.removeCursor(user.uuid);
user.cursor?.dispose();
delete user.cursor;
this.selectionManager?.removeSelection(user.uuid);
user.selection?.dispose();
delete user.selection;
}
while ( !this.cursorManager ) {
await new Promise(r => setTimeout(r, 500));
}
this.remoteUsers = remoteUsers;
for ( const user of this.remoteUsers ) {
user.cursor = this.cursorManager?.addCursor(user.uuid, user.color, user.display);
user.cursor?.setOffset(0);
user.cursor?.show();
user.selection = this.selectionManager?.addSelection(user.uuid, user.color);
}
}
public onEditorModelChange($event) {
if ( this.editorValue !== this.dbRecord.code ) {
this.dirty = true;
this.editorService.triggerSave();
}
}
public onSelectChange(updateDbRecord = true) {
if ( updateDbRecord ) {
this.dbRecord.Language = this.editorOptions.language;
this.editorService.triggerSave();
this.dirty = true;
}
this.editorOptions = {...this.editorOptions};
}
}

@ -1,150 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-title>Manage Database Columns</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismissModal(false)">
<ion-icon name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-grid>
<ion-row
*ngFor="let colSet of columnSets; let i = index"
class="column-def"
>
<ion-col size="5">
<ion-item>
<ion-label position="floating">Field Label</ion-label>
<ion-input type="text" required [(ngModel)]="columnSets[i].headerName"></ion-input>
</ion-item>
</ion-col>
<ion-col size="4">
<ion-item>
<ion-label position="floating">Data Type</ion-label>
<ion-select interface="popover" [(ngModel)]="columnSets[i].Type">
<ion-select-option value="text">Text</ion-select-option>
<ion-select-option value="number">Number</ion-select-option>
<ion-select-option value="paragraph">Paragraph</ion-select-option>
<ion-select-option value="wysiwyg">Rich-Text</ion-select-option>
<ion-select-option value="boolean">Boolean</ion-select-option>
<ion-select-option value="select">Select</ion-select-option>
<ion-select-option value="multiselect">Multi-Select</ion-select-option>
<ion-select-option value="datetime">Date-Time</ion-select-option>
<ion-select-option value="currency">Currency</ion-select-option>
<ion-select-option value="index">Incrementing Index</ion-select-option>
<ion-select-option value="page_link">Link to Page</ion-select-option>
<ion-select-option value="link">Hyperlink</ion-select-option>
<!-- <ion-select-option value="person">Person</ion-select-option>-->
<!-- <ion-select-option value="url">URL</ion-select-option>-->
<!-- <ion-select-option value="email">E-Mail</ion-select-option>-->
</ion-select>
</ion-item>
</ion-col>
<ion-col size="3" align-items-center>
<ion-row>
<ion-button fill="outline" color="light" (click)="onDeleteClick(i)">
<ion-icon color="danger" name="trash"></ion-icon>
</ion-button>
<ion-button
fill="outline"
color="light"
(click)="iteratePin(i)"
[title]="columnSets[i].additionalData.pinned ? 'Column is pinned to the ' + columnSets[i].additionalData.pinned : 'Column is not pinned'"
>
<i
class="fa fa-caret-square-left"
style="font-size: 1.7em; color: #cccccc" *ngIf="columnSets[i].additionalData.pinned === 'left'"
></i>
<i
class="fa fa-caret-square-right"
style="font-size: 1.7em; color: #cccccc" *ngIf="columnSets[i].additionalData.pinned === 'right'"
></i>
<i
class="fa fa-square"
style="font-size: 1.7em; color: #cccccc" *ngIf="!columnSets[i].additionalData.pinned"
></i>
</ion-button>
</ion-row>
<ion-row>
<ion-button fill="outline" color="light" size="small" (click)="onUpArrow(i)">
<ion-icon color="dark" name="arrow-up"></ion-icon>
</ion-button>
<ion-button fill="outline" color="light" size="small" (click)="onDownArrow(i)">
<ion-icon color="dark" name="arrow-down"></ion-icon>
</ion-button>
</ion-row>
</ion-col>
<ion-col size="5" *ngIf="columnSets[i].Type === 'boolean'">
<ion-item>
<ion-label position="floating">Label Type</ion-label>
<ion-select interface="popover" [(ngModel)]="columnSets[i].additionalData.labelType">
<ion-select-option value="true_false">True/False</ion-select-option>
<ion-select-option value="yes_no">Yes/No</ion-select-option>
<ion-select-option value="1_0">1/0</ion-select-option>
<ion-select-option value="checkbox">Checkbox</ion-select-option>
</ion-select>
</ion-item>
</ion-col>
<ion-col size="12" *ngIf="columnSets[i].Type === 'select' || columnSets[i].Type === 'multiselect'">
<ion-button (click)="onAddOption(i)" fill="outline">Add Option</ion-button>
<ng-container *ngIf="columnSets[i].additionalData.options">
<ion-row *ngFor="let option of columnSets[i].additionalData.options; let n = index">
<ion-col size="10">
<ion-item>
<ion-label position="floating">Value</ion-label>
<ion-input [(ngModel)]="columnSets[i].additionalData.options[n].value"></ion-input>
</ion-item>
</ion-col>
<ion-col size="2">
<ion-button fill="outline" color="light" size="small" (click)="onDeleteOptionClick(i, n)">
<ion-icon color="danger" name="trash"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ng-container>
</ion-col>
<ion-col size="12" *ngIf="columnSets[i].Type === 'datetime'">
<ion-list>
<ion-radio-group value="YYYY-MM-DD h:mm a" [(ngModel)]="columnSets[i].additionalData.format">
<ion-list-header>Format</ion-list-header>
<ion-item>
<ion-label>Date Only</ion-label>
<ion-radio slot="start" value="YYYY-MM-DD"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Time Only</ion-label>
<ion-radio slot="start" value="h:mm a"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Both</ion-label>
<ion-radio slot="start" value="YYYY-MM-DD h:mm a"></ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
</ion-col>
<ion-col size="12" *ngIf="columnSets[i].Type === 'currency'">
<ion-item>
<ion-label position="floating">Currency</ion-label>
<ion-select [(ngModel)]="columnSets[i].additionalData.currency">
<ion-select-option value="USD">US Dollar</ion-select-option>
<ion-select-option value="EUR">Euro</ion-select-option>
<ion-select-option value="MXN">Mexican Peso</ion-select-option>
<ion-select-option value="CNY">Chinese Yuan</ion-select-option>
<ion-select-option value="XAG">Silver</ion-select-option>
<ion-select-option value="XAU">Gold</ion-select-option>
</ion-select>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
<ion-footer>
<ion-buttons style="padding: 10px;">
<ion-button (click)="onAddColumnClick()" fill="outline">Add Column</ion-button>
<ion-button (click)="dismissModal(true)" color="success" fill="outline">Save</ion-button>
</ion-buttons>
</ion-footer>

@ -1,6 +0,0 @@
.column-def {
border: 2px solid #ccc;
margin: 5px;
padding: 5px;
border-radius: 7px;
}

@ -1,96 +0,0 @@
import {Component, Input, OnInit} from '@angular/core';
import {AlertController, ModalController} from '@ionic/angular';
import {uuid_v4} from '../../../../utility';
@Component({
selector: 'editor-database-columns',
templateUrl: './columns.component.html',
styleUrls: ['./columns.component.scss'],
})
export class ColumnsComponent implements OnInit {
@Input() columnSets: Array<{headerName: string, field: string, Type: string, additionalData: any}> = [];
constructor(
protected modals: ModalController,
protected alerts: AlertController,
) { }
ngOnInit() {}
onAddColumnClick() {
this.columnSets.push({headerName: '', field: uuid_v4(), Type: '', additionalData: {}});
}
onAddOption(i) {
const set = this.columnSets[i];
if ( !Array.isArray(set.additionalData.options) ) {
set.additionalData.options = [];
}
set.additionalData.options.push({value: ''});
}
onDeleteOptionClick(i, n) {
const set = this.columnSets[i];
set.additionalData.options = set.additionalData.options.filter((x, index) => index !== n);
}
dismissModal(doSave = true) {
if ( doSave ) {
this.columnSets = this.columnSets.map(x => {
if ( !x.field ) {
x.field = uuid_v4();
}
return x;
});
this.modals.dismiss(this.columnSets);
} else {
this.modals.dismiss();
}
}
onDeleteClick(i) {
this.alerts.create({
header: 'Delete column?',
message: 'Are you sure you want to delete this column? Its data will be lost.',
buttons: [
{ text: 'Keep It', role: 'cancel' },
{
text: 'Delete It',
handler: () => {
this.columnSets = this.columnSets.filter((x, index) => {
return index !== i;
});
},
}
],
}).then(alert => alert.present());
}
onUpArrow(i) {
if ( this.columnSets[i - 1] ) {
const temp = this.columnSets[i];
this.columnSets[i] = this.columnSets[i - 1];
this.columnSets[i - 1] = temp;
}
}
onDownArrow(i) {
if ( this.columnSets[i + 1] ) {
const temp = this.columnSets[i];
this.columnSets[i] = this.columnSets[i + 1];
this.columnSets[i + 1] = temp;
}
}
iteratePin(i) {
if ( !this.columnSets[i].additionalData.pinned ) {
this.columnSets[i].additionalData.pinned = 'left';
} else if ( this.columnSets[i].additionalData.pinned === 'left' ) {
this.columnSets[i].additionalData.pinned = 'right';
} else {
delete this.columnSets[i].additionalData.pinned;
}
}
}

@ -1,41 +0,0 @@
import {Component, Input, OnInit} from '@angular/core';
import {EditorService} from '../../../service/editor.service';
@Component({
selector: 'noded-database-page',
template: `
<div class="container" *ngIf="ready">
<editor-database [nodeId]="nodeId" [editorUUID]="this.editorService.instanceUUID" [fullPage]="true"></editor-database>
</div>
`,
styles: [
`
.container {
height: 100%;
}
editor-database {
height: 100%;
display: flex;
}
`,
],
})
export class DatabasePageComponent implements OnInit {
@Input() nodeId: string;
@Input() pageId: string;
public ready = false;
constructor(
public editorService: EditorService,
) {
this.editorService = editorService.getEditor();
}
ngOnInit() {
this.editorService.startEditing(this.pageId).then(() => {
this.ready = true;
});
}
}

@ -1,61 +0,0 @@
<div [ngClass]="fullPage ? 'database-wrapper full-page' : 'database-wrapper'" *ngIf="!notAvailableOffline" (resized)="onResized()">
<ion-toolbar>
<div style="display: flex; flex-direction: row">
<ion-input
[readonly]="readonly"
[(ngModel)]="dbName"
(ionChange)="onCellValueChanged()"
style="flex: 1; font-size: 15pt;"
></ion-input>
<button
class="clear-btn"
style="padding: 0 20px;"
*ngIf="fullPage && (editorService.isSaving || editorService.willSave)"
title="Saving..."
>
<i class="fa fa-spin fa-circle-notch"></i>
</button>
<button
class="clear-btn"
style="padding: 0 20px;"
*ngIf="fullPage && !(editorService.isSaving || editorService.willSave)"
title="Changes saved."
(click)="editorService.triggerSave()"
>
<i class="fa fa-check-circle"></i>
</button>
<button style="padding: 0 20px;" *ngIf="fullPage" (click)="dismiss()" class="clear-btn"><i class="fa fa-times"></i></button>
</div>
<ion-buttons *ngIf="!readonly" style="flex-wrap: wrap;">
<ion-button (click)="onManageColumns()"><ion-icon name="build" color="primary"></ion-icon>&nbsp;Manage Columns</ion-button>
<ion-button (click)="onInsertRow()"><ion-icon name="add-circle" color="success"></ion-icon>&nbsp;Insert Row</ion-button>
<ion-button (click)="onRemoveRow()" [disabled]="lastClickRow < 0"><ion-icon name="remove-circle" color="danger"></ion-icon>&nbsp;Delete Row</ion-button>
<ion-button (click)="openDatabase()" *ngIf="!fullPage"><i class="fa fa-external-link-alt" style="padding-right: 10px;"></i> Open</ion-button>
</ion-buttons>
</ion-toolbar>
<div class="grid-wrapper">
<ag-grid-angular
[style]="fullPage ? 'width: 100%; height: 100%;' : 'width: 100%; height: 500px;'"
[ngClass]="isDark() ? 'ag-theme-balham-dark' : 'ag-theme-balham'"
[rowData]="rowData"
[getRowNodeId]="getRowNodeId"
[columnDefs]="columnDefs"
[singleClickEdit]="true"
[enterMovesDownAfterEdit]="true"
[rowDragManaged]="true"
[suppressMoveWhenRowDragging]="true"
suppressMovableColumns="true"
(rowClicked)="onRowClicked($event)"
(cellValueChanged)="onCellValueChanged()"
(gridReady)="onGridReady($event)"
(columnResized)="onColumnResize($event)"
(rowDragMove)="onRowDragEnd($event)"
[frameworkComponents]="frameworkComponents"
#agGridElement
></ag-grid-angular>
</div>
</div>
<div class="database-wrapper not-available" *ngIf="notAvailableOffline">
Sorry, this database is not available offline yet.
</div>

@ -1,21 +0,0 @@
div.database-wrapper {
border: 2px solid #8c8c8c;
border-radius: 3px;
&.not-available {
height: 600px;
text-align: center;
padding-top: 100px;
color: #494949;
}
}
.full-page {
width: 100%;
display: flex;
flex-direction: column;
.grid-wrapper {
flex: 1;
}
}

@ -1,539 +0,0 @@
import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service';
import {AlertController, LoadingController, ModalController} from '@ionic/angular';
import {ColumnsComponent} from './columns/columns.component';
import {AgGridAngular} from 'ag-grid-angular';
import {NumericEditorComponent} from './editors/numeric/numeric-editor.component';
import {ParagraphEditorComponent} from './editors/paragraph/paragraph-editor.component';
import {BooleanEditorComponent} from './editors/boolean/boolean-editor.component';
import {SelectEditorComponent} from './editors/select/select-editor.component';
import {MultiSelectEditorComponent} from './editors/select/multiselect-editor.component';
import {DatetimeEditorComponent} from './editors/datetime/datetime-editor.component';
import {DatetimeRendererComponent} from './renderers/datetime-renderer.component';
import {CurrencyRendererComponent} from './renderers/currency-renderer.component';
import {BooleanRendererComponent} from './renderers/boolean-renderer.component';
import {EditorNodeContract} from '../../nodes/EditorNode.contract';
import {EditorService} from '../../../service/editor.service';
import {WysiwygEditorComponent} from './editors/wysiwyg/wysiwyg-editor.component';
import {debounce, debug, uuid_v4} from '../../../utility';
import {DateTimeFilterComponent} from './filters/date-time.filter';
import {DatabasePageComponent} from './database-page.component';
import {PageLinkRendererComponent} from './renderers/page-link-renderer.component';
import {LinkRendererComponent} from './renderers/link-renderer.component';
import {PageLinkEditorComponent} from './editors/page-link/page-link-editor.component';
@Component({
selector: 'editor-database',
templateUrl: './database.component.html',
styleUrls: ['./database.component.scss'],
})
export class DatabaseComponent extends EditorNodeContract implements OnInit {
@Input() nodeId: string;
@Input() editorUUID?: string;
@Input() fullPage = false;
@ViewChild('agGridElement') agGridElement: AgGridAngular;
frameworkComponents = {
agDateInput: DateTimeFilterComponent
};
public dbRecord: any;
public pendingSetup = true;
public dirty = false;
public lastClickRow = -1;
public dbName = '';
public notAvailableOffline = false;
public pages = [];
protected dbId!: string;
protected isInitialLoad = false;
protected triggerSaveDebounce = debounce(() => {
if ( this.agGridElement.api.getCellEditorInstances().length < 1 ) {
this.editorService.triggerSave();
} else {
this.triggerSaveDebounce();
}
}, 1000);
protected gridReady = false;
protected deferredUIActivation = false;
title = 'app';
columnDefs = [];
rowData = [];
public isDark() {
return document.body.classList.contains('dark');
}
public get readonly() {
return !this.node || !this.editorService.canEdit();
}
constructor(
protected api: ApiService,
protected modals: ModalController,
protected alerts: AlertController,
protected loader: LoadingController,
public editorService: EditorService,
) { super(); }
public isDirty(): boolean | Promise<boolean> {
return this.dirty;
}
public needsSave(): boolean | Promise<boolean> {
return this.dirty;
}
public needsLoad(): boolean | Promise<boolean> {
return this.node && this.pendingSetup;
}
public writeChangesToNode(): void | Promise<void> {
this.node.Value.Mode = 'database';
}
async ngOnInit() {
this.pages = this.flattenItems(await this.api.getMenuItems(true));
this.editorService = this.editorService.getEditor(this.editorUUID);
this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
});
}
protected flattenItems(items: any[], level = 0) {
let newItems = [];
for ( const item of items ) {
item.level = level;
newItems.push(item);
if ( Array.isArray(item.children) ) {
newItems = [...newItems, ...this.flattenItems(item.children, level + 1)];
}
}
return newItems;
}
onGridReady($event) {
this.gridReady = true;
if ( this.deferredUIActivation && !this.pendingSetup ) {
this.performUIActivation();
}
}
onColumnResize($event) {
if ( $event.source === 'uiColumnDragged' && $event.finished ) {
debug('Column resized: ', $event);
const state = $event.columnApi.getColumnState().find(x => x.colId === $event.column.colId );
if ( state ) {
const colDef = this.columnDefs.find(x => x.field === $event.column.colId);
if ( colDef ) {
colDef.width = state.width;
this.dirty = true;
this.triggerSaveDebounce();
}
}
}
}
onRowDragEnd($event) {
if ( !this.isInitialLoad && this.editorService.canEdit() ) {
this.dirty = true;
this.triggerSaveDebounce();
}
}
onCellValueChanged() {
if ( !this.isInitialLoad ) {
this.dirty = true;
this.triggerSaveDebounce();
}
}
async onManageColumns() {
if ( this.readonly ) {
return;
}
const modal = await this.modals.create({
component: ColumnsComponent,
componentProps: {columnSets: this.columnDefs.slice(1)},
cssClass: 'modal-med',
});
modal.onDidDismiss().then(result => {
if ( result?.data ) {
this.setColumns(result.data);
}
});
const modalState = {
modal : true,
desc : 'Manage Columns'
};
history.pushState(modalState, null);
await modal.present();
}
onInsertRow() {
if ( this.readonly ) {
return;
}
this.rowData.push({});
this.agGridElement.api.setRowData(this.rowData);
this.dirty = true;
this.triggerSaveDebounce();
}
async onRemoveRow() {
if ( this.readonly ) {
return;
}
const alert = await this.alerts.create({
header: 'Are you sure?',
message: `You are about to delete row ${this.lastClickRow + 1}. This cannot be undone.`,
buttons: [
{
text: 'Keep It',
role: 'cancel',
},
{
text: 'Delete It',
handler: () => {
this.rowData = this.rowData.filter((x, i) => {
return i !== this.lastClickRow;
});
this.agGridElement.api.setRowData(this.rowData);
this.lastClickRow = -1;
this.dirty = true;
this.triggerSaveDebounce();
},
}
],
});
await alert.present();
}
onRowClicked($event) {
this.lastClickRow = $event.rowIndex;
}
setColumns(data, triggerSave = true) {
this.columnDefs = [{
width: 20,
// rowDrag: !this.readonly,
// rowDragText: (params, dragItemCount) => `${dragItemCount} ${dragItemCount === 1 ? 'row' : 'rows'}`,
}, ...data.map(x => {
x.editable = !this.readonly;
x.minWidth = 150;
x.resizable = true;
x._parentEditorUUID = this.editorUUID;
if ( x.additionalData?.width ) {
x.width = x.additionalData.width;
}
if ( x.additionalData?.pinned ) {
x.pinned = x.additionalData.pinned;
}
// Set editors and renderers for different types
if ( x.Type === 'text' ) {
x.editor = 'agTextCellEditor';
x.filter = 'agTextColumnFilter';
} else if ( x.Type === 'number' ) {
x.cellEditorFramework = NumericEditorComponent;
x.filter = 'agNumberColumnFilter';
} else if ( x.Type === 'paragraph' ) {
x.cellEditorFramework = ParagraphEditorComponent;
x.filter = 'agTextColumnFilter';
} else if ( x.Type === 'boolean' ) {
x.cellRendererFramework = BooleanRendererComponent;
x.cellEditorFramework = BooleanEditorComponent;
x.suppressSizeToFit = true;
} else if ( x.Type === 'select' ) {
x.cellEditorFramework = SelectEditorComponent;
x.filter = 'agTextColumnFilter';
} else if ( x.Type === 'multiselect' ) {
x.cellEditorFramework = MultiSelectEditorComponent;
x.filter = 'agTextColumnFilter';
} else if ( x.Type === 'datetime' ) {
x.cellEditorFramework = DatetimeEditorComponent;
x.cellRendererFramework = DatetimeRendererComponent;
x.filter = 'agDateColumnFilter';
x.filterParams = {
buttons: ['apply', 'clear'],
displayFormat: x.additionalData.format,
comparator: (filterDate: Date, cellValue) => {
if ( !cellValue ) {
return 0;
}
const cellDate = new Date(cellValue);
if ( x.additionalData.format === 'YYYY-MM-DD' ) {
cellDate.setHours(0);
cellDate.setMinutes(0);
cellDate.setSeconds(0);
cellDate.setMilliseconds(0);
} else if ( x.additionalData.format === 'h:mm a' ) {
cellDate.setFullYear(filterDate.getFullYear());
cellDate.setMonth(filterDate.getMonth());
cellDate.setDate(filterDate.getDate());
}
// Now that both parameters are Date objects, we can compare
if (cellDate < filterDate) {
return -1;
} else if (cellDate > filterDate) {
return 1;
} else {
return 0;
}
},
};
} else if ( x.Type === 'currency' ) {
x.cellEditorFramework = NumericEditorComponent;
x.cellRendererFramework = CurrencyRendererComponent;
x.filter = 'agNumberColumnFilter';
} else if ( x.Type === 'index' ) {
x.editable = false;
x.suppressSizeToFit = true;
x.filter = 'agNumberColumnFilter';
if ( !x.width ) {
x.width = 80;
}
x.minWidth = 80;
} else if ( x.Type === 'wysiwyg' ) {
x.cellEditorFramework = WysiwygEditorComponent;
x.filter = 'agTextColumnFilter';
} else if ( x.Type === 'page_link' ) {
x.cellRendererFramework = PageLinkRendererComponent;
x.cellEditorFramework = PageLinkEditorComponent;
x.filter = 'agTextColumnFilter';
if ( !x.cellEditorParams ) {
x.cellEditorParams = {} as any;
}
if ( !x.cellRendererParams ) {
x.cellRendererParams = {} as any;
}
x.cellEditorParams._pagesData = this.pages;
x.cellRendererParams._pagesData = this.pages;
} else if ( x.Type === 'link' ) {
x.cellRendererFramework = LinkRendererComponent;
x.editor = 'agTextCellEditor';
x.filter = 'agTextColumnFilter';
}
return x;
})];
this.agGridElement.api.setColumnDefs([]);
this.agGridElement.api.setColumnDefs(this.columnDefs);
this.agGridElement.api.sizeColumnsToFit();
if ( triggerSave ) {
this.dirty = true;
this.triggerSaveDebounce();
}
}
public onResized() {
this.agGridElement.api.sizeColumnsToFit();
}
public async performLoad(): Promise<void> {
this.isInitialLoad = true;
if ( !this.node.Value ) {
this.node.Value = {};
}
// Load the database record itself
if ( !this.node.Value.Value && this.editorService.canEdit() ) {
this.dbRecord = await this.api.createDatabase(this.page.UUID, this.node.UUID);
this.dbName = this.dbRecord.Name;
this.node.Value.Mode = 'database';
this.node.Value.Value = this.dbRecord.UUID;
this.node.value = this.dbRecord.UUID;
} else {
try {
this.dbRecord = await this.api.getDatabase(
this.page.UUID, this.node.UUID, this.node.Value.Value, this.node.associatedTypeVersionNum);
this.dbName = this.dbRecord.Name;
this.notAvailableOffline = false;
} catch (e: unknown) {
if ( e instanceof ResourceNotAvailableOfflineError ) {
this.notAvailableOffline = true;
} else {
throw e;
}
}
}
// Load the columns
const columns = await this.api.getDatabaseColumns(
this.page.UUID, this.node.UUID, this.node.Value.Value, this.node.associatedTypeVersionNum);
this.setColumns(columns, false);
const rows = await this.api.getDatabaseEntries(this.page.UUID, this.node.UUID, this.node.Value.Value);
this.rowData = rows.map(x => x.RowData);
this.agGridElement.api.setRowData(this.rowData);
this.pendingSetup = false;
this.dirty = false;
this.isInitialLoad = false;
if ( this.deferredUIActivation && this.gridReady ) {
await this.performUIActivation();
}
}
public async performDelete(): Promise<void> {
await this.api.deleteDatabase(this.page.UUID, this.node.UUID, this.node.Value.Value);
}
public getSaveColumns() {
return this.columnDefs.slice(1).map(x => {
if ( !x.additionalData ) {
x.additionalData = {};
}
if ( x.width ) {
x.additionalData.width = x.width;
}
return x;
});
}
public getRowNodeId(data: any) {
if ( !data.UUID ) {
data.UUID = uuid_v4();
}
return data.UUID;
}
protected getAllRows() {
const rowData: any[] = [];
this.agGridElement.api.forEachNode(node => {
if ( !node.data.UUID ) {
node.data.UUID = uuid_v4();
}
rowData.push(node.data);
});
return rowData;
}
public async performSave(): Promise<void> {
// Save the columns first
await this.api.saveDatabaseColumns(this.page.UUID, this.node.UUID, this.node.Value.Value, this.getSaveColumns());
// Save the data
const allRows = this.getAllRows();
const rows = await this.api.saveDatabaseEntries(this.page.UUID, this.node.UUID, this.node.Value.Value, allRows);
// this.rowData = rows.map(x => x.RowData);
// Dynamically update the row data to avoid breaking open editors
const returnedUUIDs = rows.map(x => x.UUID);
const existingUUIDs = [];
const rowDataTransaction = {
add: [],
remove: [],
update: [],
};
this.agGridElement.api.forEachNode((rowNode, index) => {
const data = rowNode.data;
if ( !returnedUUIDs.includes(data.UUID) ) {
rowDataTransaction.remove.push(rowNode.id);
} else {
existingUUIDs.push(data.UUID);
const updatedRow = rows.find(x => x.UUID === data.UUID);
if ( updatedRow ) {
for ( const prop in updatedRow ) {
if ( !updatedRow.hasOwnProperty(prop) ) {
continue;
}
data[prop] = updatedRow[prop];
}
rowDataTransaction.update.push(data);
}
}
});
// for ( const row of rows ) {
// if ( !gridUUIDs.includes(row.UUID) ) {
// rowDataTransaction.add.push(row);
// }
// }
// @ts-ignore
this.agGridElement.api.applyTransaction(rowDataTransaction);
// this.agGridElement.api.setRowData(this.rowData);
// Save the name
await this.api.saveDatabaseName(this.page.UUID, this.node.UUID, this.node.Value.Value, this.dbName);
this.dirty = false;
}
async openDatabase() {
const modal = await this.modals.create({
component: DatabasePageComponent,
componentProps: {
nodeId: this.nodeId,
pageId: this.editorService.currentPageId,
},
cssClass: 'modal-big',
});
modal.onDidDismiss().then(() => {
this.editorService.reload();
});
const modalState = {
modal : true,
desc : 'Open Database'
};
history.pushState(modalState, null);
await modal.present();
}
performUIActivation() {
if ( this.deferredUIActivation ) {
this.deferredUIActivation = false;
}
if ( this.gridReady && !this.pendingSetup ) {
return this.openDatabase();
} else {
this.deferredUIActivation = true;
}
}
dismiss() {
this.modals.dismiss();
}
}

@ -1,68 +0,0 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {debounce} from '../../../../../utility';
@Component({
selector: 'cell-editor-paragraph',
template: `<input #input [value]="display" readonly (click)="onClick()">`,
styles: [
`input {
width: 100%;
border: 1px solid grey;
}`
],
})
export class BooleanEditorComponent implements ICellEditorAngularComp, AfterViewInit {
private params: ICellEditorParams;
public value: boolean;
public get display() {
if ( typeof this.value === 'undefined' ) {
return this.emptyValue;
} else if ( this.value ) {
return this.trueValue;
} else {
return this.falseValue;
}
}
protected trueValue = 'True';
protected falseValue = 'False';
protected emptyValue = '';
protected autoDismissDebounce = debounce(() => {
this.params.stopEditing();
}, 2000);
@ViewChild('input') input: ElementRef;
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
// @ts-ignore
const values = params.colDef.additionalData.labelType.split('_');
this.trueValue = values[0].charAt(0).toUpperCase() + values[0].slice(1);
this.falseValue = values[1].charAt(0).toUpperCase() + values[1].slice(1);
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
this.onClick();
}
onClick() {
if ( this.value === true ) {
this.value = false;
} else if ( this.value === false ) {
this.value = undefined;
} else {
this.value = true;
}
this.autoDismissDebounce();
}
}

@ -1,39 +0,0 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {IonDatetime} from '@ionic/angular';
@Component({
selector: 'cell-editor-select',
template: `<ion-datetime
#picker [displayFormat]="format" [(ngModel)]="value" style="padding: 0 0 0 5px;" (ionChange)="finishEdit($event)"
></ion-datetime>`,
})
export class DatetimeEditorComponent implements ICellEditorAngularComp, AfterViewInit {
public params: ICellEditorParams;
public value: string;
public format = 'YYYY-MM-DD h:mm a';
@ViewChild('picker') picker: IonDatetime;
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
// @ts-ignore
if ( this.params.colDef.additionalData.format ) {
// @ts-ignore
this.format = this.params.colDef.additionalData.format;
}
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
this.picker.open();
}
finishEdit($event) {
this.params.stopEditing();
}
}

@ -1,75 +0,0 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
@Component({
selector: 'cell-editor-numeric',
template: `<input #input (keydown)="onKeyDown($event)" [(ngModel)]="value">`,
styles: [
`input {
width: 100%;
border: 1px solid grey;
}`
],
})
export class NumericEditorComponent implements ICellEditorAngularComp, AfterViewInit {
private params: ICellEditorParams;
public value: number;
private cancelBeforeStart = false;
@ViewChild('input') input: ElementRef;
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
// Only cancel before start if the pressed key is numeric
this.cancelBeforeStart = params.charPress && ('1234567890'.indexOf(params.charPress) < 0);
}
getValue(): any {
return this.value;
}
isCancelBeforeStart(): boolean {
return this.cancelBeforeStart;
}
onKeyDown($event: KeyboardEvent) {
if ( !this.isKeyPressedAllowed($event) ) {
if ($event.preventDefault) {
$event.preventDefault();
}
}
}
ngAfterViewInit(): void {
setTimeout(() => {
this.input.nativeElement.focus();
});
}
private getCharCodeFromEvent(event): any {
return (typeof event.which === 'undefined') ? event.keyCode : event.which;
}
private isCharNumeric(charStr): boolean {
return !!/\d/.test(charStr);
}
private isKeyPressedAllowed(event): boolean {
const charCode = this.getCharCodeFromEvent(event);
const charStr = event.key ? event.key : String.fromCharCode(charCode);
if (this.isCharNumeric(charStr)) {
return true;
} else if ( charStr === '.' && this.value % 1 === 0 ) {
return true;
} else if ( ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Enter'].includes(event.code) ) {
return true;
} else if ( charStr === 'a' && event.ctrlKey ) {
return true;
}
return false;
}
}

@ -1,14 +0,0 @@
<ionic-selectable
#selectable
[items]="pages"
itemTextField="name"
itemValueField="id"
[canSearch]="true"
[title]="'Select a page'"
[(ngModel)]="value"
(onClose)="finishEdit()"
>
<ng-template ionicSelectableItemTemplate let-port="item" let-isPortSelected="itemIsSelected">
<div [ngStyle]="{ marginLeft: (20 * port.level) + 'px' }">{{ port.name }}</div>
</ng-template>
</ionic-selectable>

@ -1,42 +0,0 @@
import {AfterViewInit, Component, OnInit, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {ApiService} from '../../../../../service/api.service';
import {IonicSelectableComponent} from "ionic-selectable";
@Component({
selector: 'editor-cel-page-link',
templateUrl: './page-link-editor.component.html',
styleUrls: ['./page-link-editor.component.scss'],
})
export class PageLinkEditorComponent implements OnInit {
@ViewChild('selectable') selectable: IonicSelectableComponent;
public params: ICellEditorParams;
public value: any;
public pages: any[] = [];
constructor(
public readonly api: ApiService,
) { }
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = { id: this.params.value };
// @ts-ignore
this.pages = params._pagesData || [];
setTimeout(() => {
this.selectable.open();
});
}
getValue(): any {
return this.value.id;
}
async ngOnInit() {}
finishEdit() {
this.params.stopEditing();
}
}

@ -1,69 +0,0 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {ModalController} from '@ionic/angular';
import {ParagraphModalComponent} from './paragraph-modal.component';
@Component({
selector: 'cell-editor-paragraph',
template: `<input #input [(ngModel)]="value" readonly>`,
styles: [
`input {
width: 100%;
border: 1px solid grey;
}`
],
})
export class ParagraphEditorComponent implements ICellEditorAngularComp, AfterViewInit {
private params: ICellEditorParams;
public value: string;
@ViewChild('input') input: ElementRef;
constructor(
protected modals: ModalController,
) { }
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
setTimeout(() => {
this.modals.create({
component: ParagraphModalComponent,
componentProps: {
title: this.params.colDef.headerName,
value: this.value,
}
}).then(modal => {
modal.onDidDismiss().then(value => {
if ( typeof value.data === 'undefined' ) {
return;
}
this.value = String(value.data);
this.finishEdit();
});
const modalState = {
modal : true,
desc : 'Paragraph editor'
};
history.pushState(modalState, null);
modal.present();
});
});
}
finishEdit() {
this.params.stopEditing();
}
}

@ -1,27 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ title }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismissModal()">
<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">Content</ion-label>
<ion-textarea [(ngModel)]="value" rows="15"></ion-textarea>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
<ion-footer>
<ion-button fill="invisible" (click)="dismissModal()"><ion-icon name="save"></ion-icon>&nbsp;&nbsp;Save</ion-button>
</ion-footer>

@ -1,20 +0,0 @@
import {Component, Input} from '@angular/core';
import {ModalController} from '@ionic/angular';
@Component({
selector: 'editor-paragraph-modal',
templateUrl: './paragraph-modal.component.html',
styleUrls: ['./paragraph-modal.component.scss'],
})
export class ParagraphModalComponent {
@Input() value = '';
@Input() title: string;
constructor(
protected modals: ModalController,
) {}
dismissModal() {
this.modals.dismiss(this.value);
}
}

@ -1,40 +0,0 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {IonSelect} from '@ionic/angular';
@Component({
selector: 'cell-editor-multiselect',
template: `
<ion-select #select [(ngModel)]="value" style="padding: 0;" (ionChange)="finishEdit()"
[interfaceOptions]="{header: params.colDef.headerName, cssClass: 'big-alert'}" multiple="true">
<ion-select-option *ngFor="let option of options" [value]="option.value">{{ option.value }}</ion-select-option>
</ion-select>
`,
})
export class MultiSelectEditorComponent implements ICellEditorAngularComp, AfterViewInit {
public params: ICellEditorParams;
public value: string;
public options: Array<{value: string}> = [];
@ViewChild('select') select: IonSelect;
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
// @ts-ignore
this.options = this.params.colDef.additionalData.options;
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
this.select.open();
}
finishEdit() {
this.params.stopEditing();
}
}

@ -1,39 +0,0 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {IonSelect} from '@ionic/angular';
@Component({
selector: 'cell-editor-select',
template: `
<ion-select #select [(ngModel)]="value" style="padding: 0;" (ionChange)="finishEdit()"
[interfaceOptions]="{header: params.colDef.headerName, cssClass: 'big-alert'}">
<ion-select-option *ngFor="let option of options" [value]="option.value">{{ option.value }}</ion-select-option>
</ion-select>
`,
})
export class SelectEditorComponent implements ICellEditorAngularComp, AfterViewInit {
public params: ICellEditorParams;
public value: string;
public options: Array<{value: string}> = [];
@ViewChild('select') select: IonSelect;
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
// @ts-ignore
this.options = this.params.colDef.additionalData.options;
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
this.select.open();
}
finishEdit() {
this.params.stopEditing();
}
}

@ -1,69 +0,0 @@
import {ICellEditorAngularComp} from 'ag-grid-angular';
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {ICellEditorParams} from 'ag-grid-community';
import {ModalController} from '@ionic/angular';
import {WysiwygModalComponent} from './wysiwyg-modal.component';
@Component({
selector: 'cell-editor-wysiwyg',
template: `<input #input [(ngModel)]="value" readonly>`,
styles: [
`input {
width: 100%;
border: 1px solid grey;
}`
],
})
export class WysiwygEditorComponent implements ICellEditorAngularComp, AfterViewInit {
private params: ICellEditorParams;
public value: string;
@ViewChild('input') input: ElementRef;
constructor(
protected modals: ModalController,
) { }
agInit(params: ICellEditorParams): void {
this.params = params;
this.value = this.params.value;
}
getValue(): any {
return this.value;
}
ngAfterViewInit(): void {
setTimeout(() => {
this.modals.create({
component: WysiwygModalComponent,
componentProps: {
title: this.params.colDef.headerName,
value: this.value,
}
}).then(modal => {
modal.onDidDismiss().then(value => {
if ( typeof value.data === 'undefined' ) {
return;
}
this.value = String(value.data);
this.finishEdit();
});
const modalState = {
modal : true,
desc : 'WYSIWYG editor'
};
history.pushState(modalState, null);
modal.present();
});
});
}
finishEdit() {
this.params.stopEditing();
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save