16,971 changes: 16,971 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions package.json
@@ -0,0 +1,76 @@
{
"name": "fantasiavault",
"version": "0.0.1",
"description": "A database manager for world building",
"productName": "Fantasia archive",
"author": "Elvanos <elvanos66@gmail.com>",
"private": true,
"scripts": {
"lint": "eslint --ext .js,.ts,.vue ./",
"test": "echo \"No test specified\" && exit 0",
"dev": "quasar dev -m electron",
"build": "quasar build -m electron"
},
"dependencies": {
"@quasar/extras": "^1.0.0",
"axios": "^0.18.1",
"core-js": "^3.6.5",
"katex": "^0.12.0",
"lodash": "^4.17.20",
"mermaid": "^8.8.4",
"pouchdb": "^7.2.2",
"pouchdb-adapter-idb": "^7.2.2",
"pouchdb-find": "^7.2.2",
"pouchdb-load": "^1.4.6",
"pouchdb-purge-on-delete": "^7.0.1",
"pouchdb-replication-stream": "^1.2.9",
"quasar": "^1.15.0",
"quasar-tiptap": "^1.9.1",
"tiptap": "^1.31.0",
"tiptap-extensions": "^1.34.0",
"vue-class-component": "^7.2.2",
"vue-codemirror": "^4.0.6",
"vue-i18n": "^8.0.0",
"vue-property-decorator": "^8.3.0",
"vuex-class": "^0.3.2"
},
"devDependencies": {
"@quasar/app": "^2.0.0",
"@types/lodash": "^4.14.166",
"@types/node": "^10.17.15",
"@types/pouchdb": "^6.4.0",
"@typescript-eslint/eslint-plugin": "^3.3.0",
"@typescript-eslint/parser": "^3.3.0",
"babel-eslint": "^10.0.1",
"devtron": "^1.4.0",
"electron": "^9.4.0",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.1.1",
"eslint": "^6.8.0",
"eslint-config-standard": "^14.1.0",
"eslint-loader": "^3.0.3",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.1.2",
"quasar-app-extension-qdraggabletree": "0.0.5",
"typescript": "^3.8.3"
},
"browserslist": [
"last 10 Chrome versions",
"last 10 Firefox versions",
"last 4 Edge versions",
"last 7 Safari versions",
"last 8 Android versions",
"last 8 ChromeAndroid versions",
"last 8 FirefoxAndroid versions",
"last 10 iOS versions",
"last 5 Opera versions"
],
"engines": {
"node": ">= 10.18.1",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
}
}
Binary file added public/favicon.ico
Binary file not shown.
Binary file added public/icons/favicon-128x128.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/favicon-16x16.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/favicon-32x32.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/favicon-96x96.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
225 changes: 225 additions & 0 deletions quasar.conf.js
@@ -0,0 +1,225 @@
/*
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
* the ES6 features that are supported by your Node version. https://node.green/
*/

// Configuration for your app
// https://quasar.dev/quasar-cli/quasar-conf-js
/* eslint-env node */
/* eslint-disable @typescript-eslint/no-var-requires */
const { configure } = require("quasar/wrappers")

module.exports = configure(function (ctx) {
return {
// https://quasar.dev/quasar-cli/supporting-ts
supportTS: {
tsCheckerConfig: {
eslint: true
}
},

// https://quasar.dev/quasar-cli/prefetch-feature
// preFetch: true,

// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://quasar.dev/quasar-cli/boot-files
boot: [
"i18n",
"axios"
],

// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
css: [
"app.scss"
],

// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
"mdi-v5",
// 'fontawesome-v5',
// 'eva-icons',
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!

"roboto-font", // optional, you are not bound to it
"material-icons" // optional, you are not bound to it
],

// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
build: {
vueRouterMode: "history", // available values: 'hash', 'history'

// transpile: false,

// Add dependencies for transpiling with Babel (Array of string/regex)
// (from node_modules, which are by default not transpiled).
// Applies only if "transpile" is set to true.
// transpileDependencies: [],

// rtl: false, // https://quasar.dev/options/rtl-support
// preloadChunks: true,
// showProgress: false,
// gzip: true,
// analyze: true,

// Options below are automatically set depending on the env, set them if you want to override
// extractCSS: false,

// https://quasar.dev/quasar-cli/handling-webpack
extendWebpack (cfg) {
// linting is slow in TS projects, we execute it only for production builds
if (ctx.prod) {
cfg.module.rules.push({
enforce: "pre",
test: /\.(js|vue)$/,
loader: "eslint-loader",
exclude: /node_modules/
})
}
}
},

// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
devServer: {
https: false,
port: 8080,
open: true // opens browser window automatically
},

// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
framework: {
iconSet: "material-icons", // Quasar icon set
lang: "en-us", // Quasar language pack
config: {},

// Possible values for "importStrategy":
// * 'auto' - (DEFAULT) Auto-import needed Quasar components & directives
// * 'all' - Manually specify what to import
importStrategy: "auto",

// For special cases outside of where "auto" importStrategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
components: [
'QAvatar',
'QBtn',
'QBtnDropdown',
'QIcon',
'QInput',
'QItem',
'QItemLabel',
'QItemSection',
'QList',
'QMenu',
'QScrollArea',
'QScrollObserver',
'QSeparator',
'QSpinner',
'QTooltip',
],
directives: [
'ClosePopup'
],

// Quasar plugins
plugins: ['AppFullscreen', 'Notify']
},

// animations: 'all', // --- includes all animations
// https://quasar.dev/options/animations
animations: 'all',

// https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
ssr: {
pwa: false
},

// https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
pwa: {
workboxPluginMode: "GenerateSW", // 'GenerateSW' or 'InjectManifest'
workboxOptions: {}, // only for GenerateSW
manifest: {
name: "Fantasia archive",
short_name: "Fantasia archive",
description: "A database manager for world building",
display: "standalone",
orientation: "portrait",
background_color: "#ffffff",
theme_color: "#027be3",
icons: [
{
src: "icons/icon-128x128.png",
sizes: "128x128",
type: "image/png"
},
{
src: "icons/icon-192x192.png",
sizes: "192x192",
type: "image/png"
},
{
src: "icons/icon-256x256.png",
sizes: "256x256",
type: "image/png"
},
{
src: "icons/icon-384x384.png",
sizes: "384x384",
type: "image/png"
},
{
src: "icons/icon-512x512.png",
sizes: "512x512",
type: "image/png"
}
]
}
},

// Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},

// Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},

// Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
electron: {
bundler: "packager", // 'packager' or 'builder'

packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options

// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',

// Windows only
// win32metadata: { ... }
},

builder: {
// https://www.electron.build/configuration/configuration

appId: "fantasiavault"
},

// More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
nodeIntegration: true,

extendWebpack (/* cfg */) {
// do something with Electron main process Webpack cfg
// chainWebpack also available besides this extendWebpack
}
}
}
})
3 changes: 3 additions & 0 deletions quasar.extensions.json
@@ -0,0 +1,3 @@
{
"qdraggabletree": {}
}
9 changes: 9 additions & 0 deletions src-electron/electron-flag.d.ts
@@ -0,0 +1,9 @@
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";

declare module "quasar/dist/types/feature-flag" {
interface QuasarFeatureFlags {
electron: true;
}
}
Binary file added src-electron/icons/icon.icns
Binary file not shown.
Binary file added src-electron/icons/icon.ico
Binary file not shown.
Binary file added src-electron/icons/linux-512x512.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions src-electron/main-process/electron-main.dev.js
@@ -0,0 +1,41 @@
/**
* This file is used specifically and only for development. It installs
* `electron-debug` & `vue-devtools`. There shouldn't be any need to
* modify this file, but it can be used to extend your development
* environment.
*/

import electronDebug from 'electron-debug'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
import { app, BrowserWindow } from 'electron'

app.whenReady().then(() => {
// allow for a small delay for mainWindow to be created
setTimeout(() => {
// Install `electron-debug` with `devtron`
electronDebug({ showDevTools: false })

// Install vuejs devtools
installExtension(VUEJS_DEVTOOLS)
.then(name => {
console.log(`Added Extension: ${name}`)
// get main window
const win = BrowserWindow.getFocusedWindow()
if (win) {
win.webContents.on('did-frame-finish-load', () => {
win.webContents.once('devtools-opened', () => {
win.webContents.focus()
})
// open electron debug
console.log('Opening dev tools')
win.webContents.openDevTools()
})
}
})
.catch(err => {
console.log('An error occurred: ', err)
})
}, 250)
})

import './electron-main'
93 changes: 93 additions & 0 deletions src-electron/main-process/electron-main.js
@@ -0,0 +1,93 @@
import { app, BrowserWindow, nativeTheme, Menu, MenuItem } from 'electron'

try {
if (process.platform === 'win32' && nativeTheme.shouldUseDarkColors === true) {
require('fs').unlinkSync(require('path').join(app.getPath('userData'), 'DevTools Extensions'))
}
} catch (_) { }

/**
* Set `__statics` path to static files in production;
* The reason we are setting it here is that the path needs to be evaluated at runtime
*/
if (process.env.PROD) {
global.__statics = __dirname
}

let mainWindow

function createWindow () {
/**
* Initial window options
*/
mainWindow = new BrowserWindow({
useContentSize: true,
webPreferences: {
// Change from /quasar.conf.js > electron > nodeIntegration;
// More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
nodeIntegration: process.env.QUASAR_NODE_INTEGRATION,
nodeIntegrationInWorker: process.env.QUASAR_NODE_INTEGRATION,
disableBlinkFeatures: "Auxclick",
enableRemoteModule: true

// More info: /quasar-cli/developing-electron-apps/electron-preload-script
// preload: path.resolve(__dirname, 'electron-preload.js')
}
})

mainWindow.setMenu(null)
mainWindow.maximize()

mainWindow.loadURL(process.env.APP_URL)

mainWindow.on('closed', () => {
mainWindow = null
})

mainWindow.webContents.on('will-navigate', event => {
event.preventDefault()
})

mainWindow.webContents.on('new-window', event => {
event.preventDefault()
})

mainWindow.webContents.on('context-menu', (event, params) => {
const menu = new Menu()

// Add each spelling suggestion
for (const suggestion of params.dictionarySuggestions) {
menu.append(new MenuItem({
label: suggestion,
click: () => mainWindow.webContents.replaceMisspelling(suggestion)
}))
}

// Allow users to add the misspelled word to the dictionary
if (params.misspelledWord) {
menu.append(
new MenuItem({
label: 'Add to dictionary',
click: () => mainWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
})
)
}

menu.popup()
})
}

app.on('ready', createWindow)

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})

15 changes: 15 additions & 0 deletions src/App.vue
@@ -0,0 +1,15 @@
<template>
<div id="q-app">
<router-view />
</div>
</template>

<script lang="ts">
import BaseClass from "src/BaseClass"
import { Component } from "vue-property-decorator"
@Component
export default class App extends BaseClass {
}
</script>
216 changes: 216 additions & 0 deletions src/BaseClass.ts
@@ -0,0 +1,216 @@
import { I_OpenedDocument } from "./interfaces/I_OpenedDocument"
import { Component, Vue } from "vue-property-decorator"
import { namespace } from "vuex-class"
import { remote } from "electron"
// @ts-ignore
import replicationStream from "pouchdb-replication-stream/dist/pouchdb.replication-stream.min.js"
// @ts-ignore
import load from "pouchdb-load"
import PouchDB from "pouchdb"
import fs from "fs"
import path from "path"

import { I_Blueprint } from "src/interfaces/I_Blueprint"
import { I_NewObjectTrigger } from "src/interfaces/I_NewObjectTrigger"
import { uid } from "quasar"
import { I_FieldRelationship } from "src/interfaces/I_FieldRelationship"

const Blueprints = namespace("blueprintsModule")
const OpenedDocuments = namespace("openedDocumentsModule")

@Component
export default class BaseClass extends Vue {
/****************************************************************/
// Project management
/****************************************************************/
async createNewProject (projectName: string) {
await this.removeCurrentProject()

const ProjectDB = new PouchDB("project-data")
const newProject = { _id: projectName }
await ProjectDB.put(newProject)

this.$router.push({ path: "/project" }).catch((e: {name: string}) => {
const errorName : string = e.name
if (errorName === "NavigationDuplicated") {
return
}
console.log(e)
})
}

exportProject (projectName: string) {
/*eslint-disable */
remote.dialog.showOpenDialog({
properties: ["openDirectory"]
}).then(async (result) => {
const folderPath = result.filePaths[0]

PouchDB.plugin(replicationStream.plugin)
//@ts-ignore
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)

//@ts-ignore
const allDBS = await indexedDB.databases()

const DBnames: string[] = allDBS.map((db: {name: string}) => {
return db.name.replace("_pouch_", "")
})

for (const db of DBnames) {
const CurrentDB = new PouchDB(db)

if (!fs.existsSync(`${folderPath}/${projectName}`)) {
fs.mkdirSync(`${folderPath}/${projectName}`)
}
const ws = fs.createWriteStream(`${folderPath}/${projectName}/${db}.txt`)

//@ts-ignore
await CurrentDB.dump(ws)
}
}).catch(err => {
console.log(err)
})
/* eslint-enable */
}

async removeCurrentProject () {
/*eslint-disable */
//@ts-ignore
const allDBS = await indexedDB.databases()

const DBnames: string[] = allDBS.map((db: {name: string}) => {
return db.name.replace("_pouch_", "")
})

for (const db of DBnames) {
const CurrentDB = new PouchDB(db)
await CurrentDB.destroy()
}
/* eslint-enable */
}

openExistingProject () {
/*eslint-disable */
remote.dialog.showOpenDialog({
properties: ["openDirectory"]
}).then(async (result) => {

const folderPath = result.filePaths[0]

if(!folderPath){return}

await this.removeCurrentProject()

//@ts-ignore
PouchDB.plugin({
loadIt: load.load
})

const allFiles = fs.readdirSync(folderPath)

for (const file of allFiles) {
const currentDBName = path.parse(file).name
const CurrentDB = new PouchDB(currentDBName)

const fileContents = fs.readFileSync(`${folderPath}/${file}`, {encoding: 'utf8'})
// @ts-ignore
await CurrentDB.loadIt(fileContents)

}
}).catch(err => {
console.log(err)
})
/* eslint-enable */
}

async retrieveCurrentProjectName () {
const ProjectDB = new PouchDB("project-data")
const projectData = await ProjectDB.allDocs({ include_docs: true })
return projectData?.rows[0]?.id
}

/****************************************************************/
// Blueprint management
/****************************************************************/

@Blueprints.Getter("getAllBlueprints") SGET_allBlueprints !: I_Blueprint[]
@Blueprints.Getter("getBlueprint") SGET_blueprint!: (type: string) => I_Blueprint

@Blueprints.Mutation("setAllBlueprints") SSET_allBlueprints!: (input: I_Blueprint[]) => void

addNewObjectType (e: I_NewObjectTrigger) {
// console.log(e.id)

this.$router.push({ path: `/project/display-content/${e._id}/${uid()}` }).catch((e: {name: string}) => {
const errorName : string = e.name
if (errorName === "NavigationDuplicated") {
return
}
console.log(e)
})
}

openExistingDocument (e:I_OpenedDocument | I_FieldRelationship) {
this.$router.push({ path: e.url }).catch((e: {name: string}) => {
const errorName : string = e.name
if (errorName === "NavigationDuplicated") {
return
}
console.log(e)
})
}

/****************************************************************/
// Open documents management
/****************************************************************/

@OpenedDocuments.Getter("getAllDocuments") SGET_allOpenedDocuments !: {timestamp: string, docs: I_OpenedDocument[]}
@OpenedDocuments.Getter("getDocument") SGET_openedDocument!: (id: string) => I_OpenedDocument

@OpenedDocuments.Mutation("addDocument") SSET_addOpenedDocument!: (input: I_OpenedDocument) => void
@OpenedDocuments.Mutation("updateDocument") SSET_updateOpenedDocument!: (input: I_OpenedDocument) => void
@OpenedDocuments.Mutation("removeDocument") SSET_removeOpenedDocument!: (input: I_OpenedDocument) => void

retrieveFieldValue (fieldDataWrapper: I_OpenedDocument, fieldID: string) : string | [] | false | I_FieldRelationship {
const fieldData = fieldDataWrapper?.extraFields
if (!fieldData) { return false }
const fieldValue = fieldData.find(f => f.id === fieldID)?.value as unknown as string
return fieldValue
}

retrieveFieldLength (fieldDataWrapper: I_OpenedDocument, fieldID: string) : number | false {
/*eslint-disable */
const fieldData = fieldDataWrapper?.extraFields
if (!fieldData) { return false }
const fieldValueLength = fieldData.find(f => f.id === fieldID)?.value.length as unknown as number
return fieldValueLength
/* eslint-enable */
}

refreshRoute () {
const remainingDocuments = this.SGET_allOpenedDocuments.docs

if (remainingDocuments.length > 0) {
const lastDocument = remainingDocuments[remainingDocuments.length - 1]
const currentRoute = this.$router.currentRoute.path

const existingDocument = this.SGET_allOpenedDocuments.docs.find(e => {
return e.url === currentRoute
})

// Prevent infite route cycling by checking if this actually exists in the open tabs
if (existingDocument) { return }

const newRoute = `/project/display-content/${lastDocument.type}/${lastDocument._id}`
if (currentRoute !== newRoute) {
this.$router.push({ path: newRoute }).catch(e => console.log(e))
}
} else {
this.$router.push({ path: "/project" }).catch((e: {name: string}) => {
if (e.name !== "NavigationDuplicated") { console.log(e) }
}
)
}
}
}
191 changes: 191 additions & 0 deletions src/assets/quasar-logo-full.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added src/boot/.gitkeep
Empty file.
13 changes: 13 additions & 0 deletions src/boot/axios.ts
@@ -0,0 +1,13 @@
import axios, { AxiosInstance } from "axios"
import { boot } from "quasar/wrappers"

declare module "vue/types/vue" {
interface Vue {
$axios: AxiosInstance;
}
}

export default boot(({ Vue }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
Vue.prototype.$axios = axios
})
23 changes: 23 additions & 0 deletions src/boot/i18n.ts
@@ -0,0 +1,23 @@
import { boot } from "quasar/wrappers"
import messages from "src/i18n"
import Vue from "vue"
import VueI18n from "vue-i18n"

declare module "vue/types/vue" {
interface Vue {
i18n: VueI18n;
}
}

Vue.use(VueI18n)

export const i18n = new VueI18n({
locale: "en-us",
fallbackLocale: "en-us",
messages
})

export default boot(({ app }) => {
// Set i18n instance on app
app.i18n = i18n
})
151 changes: 151 additions & 0 deletions src/components/Field_List.vue
@@ -0,0 +1,151 @@
<template>
<div>
<div class="flex justify-start items-center text-weight-bolder q-mb-sm q-mt-md">
<q-icon v-if="inputIcon" :name="inputIcon" size="20px" class="q-mr-md"/>
{{inputDataBluePrint.name}}
</div>

<q-list
v-if="!editMode"
dense>
<q-item v-for="(input,index) in localInput" :key="index">
<q-item-section side>
<q-icon name="mdi-menu-right" />
</q-item-section>
<q-item-section>
<span>
{{input.value}} {{(inputAffix) ? inputAffix : ''}}
<span v-if="localInput[index].affix" class="inline-block q-ml-xs text-italic text-lowercase">
({{localInput[index].affix}})
</span>
</span>
</q-item-section>
</q-item>
</q-list>

<div v-if="editMode">
<div class="row q-mb-sm"
v-for="(singleInput,index) in localInput"
:key="index"
>
<div class="col">
<q-input
v-model="localInput[index].value"
dense
@keyup="signalInput"
outlined
:suffix="(inputAffix) ? inputAffix : ''"
>
</q-input>
</div>

<div
class="q-ml-lg justify-end flex"
style="max-width: 220px; width: 220px;"
v-if="hasExtraInput">
<q-select
style="width: 100%;"
dense
:options="localExtraInput"
use-input
:hide-dropdown-icon="!editMode"
:outlined="editMode"
:borderless="!editMode"
:readonly="!editMode"
input-debounce="0"
new-value-mode="add"
v-model="localInput[index].affix"
/>
</div>

<div class="col-2 justify-end flex">
<q-btn
v-if="editMode"
color="red"
@click="removeFromList(index)"
label="Remove" />
</div>
</div>

<div class="row q-mt-lg" v-if="editMode">
<div class="col justify-start flex">
<q-btn color="primary" :label="`Add new`" @click="addNewInput" />
</div>
</div>
</div>

<q-separator color="grey q-mt-lg" />

</div>

</template>

<script lang="ts">
import { Component, Emit, Prop, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import { I_ExtraFields } from "src/interfaces/I_Blueprint"
@Component({
components: { }
})
export default class Field_List extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({ default: () => { return [] } }) readonly inputDataValue!: {
value: string
affix?: string
}[]
@Prop() readonly isNew!: boolean
@Prop() readonly editMode!: boolean
changedInput = false
localInput = [] as {
value: string
affix?: string
}[]
localExtraInput = []
@Watch("inputDataValue", { deep: true, immediate: true })
reactToInputChanges () {
this.localInput = (this.inputDataValue) ? this.inputDataValue : []
}
get inputAffix () {
return (this.inputDataBluePrint?.predefinedListExtras?.affix) || ""
}
removeFromList (index: number) {
this.localInput.splice(index, 1)
this.signalInput()
}
get inputIcon () {
return this.inputDataBluePrint?.icon
}
get hasExtraInput () {
// @ts-ignore
this.localExtraInput = this.inputDataBluePrint?.predefinedListExtras?.extraSelectValueList
return this.inputDataBluePrint?.predefinedListExtras?.extraSelectValueList
}
@Emit()
signalInput () {
this.changedInput = true
return this.localInput
}
addNewInput () {
this.localInput.push({
value: "",
affix: ""
})
this.signalInput()
}
}
</script>
196 changes: 196 additions & 0 deletions src/components/Field_MultiRelationship.vue
@@ -0,0 +1,196 @@
<template>
<div>
<div class="flex justify-start items-center text-weight-bolder q-mb-sm q-mt-md">
<q-icon v-if="inputIcon" :name="inputIcon" size="20px" class="q-mr-md"/>
{{inputDataBluePrint.name}}
<q-icon v-if="isOneWayRelationship" name="mdi-arrow-right-bold" size="16px" class="q-ml-md">
<q-tooltip>
This is a one-way relationship. <br> Editing this value <b>will not</b> have effect on the connected document/s.
</q-tooltip>
</q-icon>
<q-icon v-if="!isOneWayRelationship" name="mdi-arrow-left-right-bold" size="16px" class="q-ml-md">
<q-tooltip>
This is a two-way relationship. <br> Editing this value <b>will</b> also effect the connected document/s.
</q-tooltip>
</q-icon>

</div>

<q-list
v-if="!editMode && localInput"
dense>
<q-item
v-for="single in localInput"
:key="single._id"
clickable
class="text-primary"
@click="openExistingDocument(single)">
<q-item-section>
{{single.label}}
</q-item-section>
</q-item>
</q-list>

<div class="flex" v-if="editMode">
<q-select
style="flex-grow: 1;"
dense
:options="filteredInput"
use-input
outlined
use-chips
multiple
input-debounce="0"
v-model="localInput"
@filter="filterSelect"
@input="signalInput"
>
<template v-slot:prepend v-if="inputIcon">
<q-icon :name="inputIcon" />
</template>
<template v-slot:option="{ itemProps, itemEvents, opt }">
<q-item
v-bind="itemProps"
v-on="itemEvents"
>
<q-item-section>
<q-item-label v-html="opt.label" ></q-item-label>
</q-item-section>
<q-tooltip v-if='opt.disable'>
This option is unavailable for selection as it is already paired to another.
</q-tooltip>
</q-item>
</template>
</q-select>

</div>

<q-separator color="grey q-mt-lg" />

</div>

</template>

<script lang="ts">
import { Component, Emit, Prop, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import PouchDB from "pouchdb"
import { I_ShortenedDocument } from "src/interfaces/I_OpenedDocument"
import { I_ExtraFields } from "src/interfaces/I_Blueprint"
import { I_FieldRelationship } from "src/interfaces/I_FieldRelationship"
@Component({
components: { }
})
export default class Field_SingleRelationship extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({ default: [] }) readonly inputDataValue!: I_FieldRelationship[]
@Prop({ default: "" }) readonly currentId!: ""
@Prop() readonly isNew!: boolean
@Prop() readonly editMode!: boolean
changedInput = false
localInput = [] as unknown as I_FieldRelationship[]
@Watch("inputDataValue", { deep: true, immediate: true })
reactToInputChanges () {
// @ts-ignore
this.localInput = (this.inputDataValue) ? this.inputDataValue : []
this.reloadObjectListAndCheckIfValueExists().catch(e => console.log(e))
}
get inputIcon () {
return this.inputDataBluePrint?.icon
}
extraInput: I_FieldRelationship[] = []
filteredInput: I_FieldRelationship[] = []
filterSelect (val: string, update: (e: () => void) => void) {
if (val === "") {
update(() => {
this.filteredInput = this.extraInput
})
return
}
update(() => {
const needle = val.toLowerCase()
this.filteredInput = this.extraInput.filter(v => v.label.toLowerCase().indexOf(needle) > -1)
})
}
@Watch("inputDataBluePrint", { deep: true, immediate: true })
reactToBlueprintChanges () {
this.reloadObjectListAndCheckIfValueExists().catch(e => console.log(e))
}
@Watch("currentId")
reactToIDChanges () {
this.reloadObjectListAndCheckIfValueExists().catch(e => console.log(e))
}
get isOneWayRelationship () {
return (this.inputDataBluePrint.type === "singleToNoneRelationship" || this.inputDataBluePrint.type === "manyToNoneRelationship")
}
async reloadObjectListAndCheckIfValueExists () {
if (this.inputDataBluePrint?.relationshipSettings && this.currentId.length > 0) {
const CurrentObjectDB = new PouchDB(this.inputDataBluePrint.relationshipSettings.connectedObjectType)
const allObjects = (await CurrentObjectDB.allDocs({ include_docs: true })).rows.map((doc) => {
const objectDoc = doc.doc as unknown as I_ShortenedDocument
const pairedField = (this.inputDataBluePrint?.relationshipSettings?.connectedField) || ""
let isDisabled = false
if (pairedField.length > 0) {
const pairedFieldObject = objectDoc.extraFields.find(f => f.id === pairedField)
const pairingType = this.inputDataBluePrint.type
if (typeof pairedFieldObject?.value !== "string" && pairedFieldObject?.value !== null && pairingType === "manyToSingleRelationship") {
isDisabled = true
}
}
return {
_id: objectDoc._id,
value: objectDoc._id,
type: objectDoc.type,
disable: isDisabled,
url: `/project/display-content/${objectDoc.type}/${objectDoc._id}`,
label: objectDoc.extraFields.find(e => e.id === "name")?.value,
pairedField: pairedField
}
}) as unknown as I_FieldRelationship[]
const allObjectsWithoutCurrent: I_FieldRelationship[] = allObjects.filter((obj) => obj._id !== this.currentId)
this.localInput = (Array.isArray(this.localInput)) ? this.localInput : []
this.localInput.forEach((s, index) => {
if (s._id) {
if (!allObjectsWithoutCurrent.find(e => e._id === s._id)) {
// @ts-ignore
this.localInput.splice(index, 1)
}
}
})
this.extraInput = allObjectsWithoutCurrent
}
}
@Emit()
signalInput () {
this.changedInput = true
return this.localInput
}
}
</script>
90 changes: 90 additions & 0 deletions src/components/Field_MultiSelect.vue
@@ -0,0 +1,90 @@
<template>
<div>
<div class="flex justify-start items-center text-weight-bolder q-mb-sm q-mt-md">
<q-icon v-if="inputIcon" :name="inputIcon" size="20px" class="q-mr-md"/>
{{inputDataBluePrint.name}}
</div>

<q-list
v-if="!editMode"
dense>
<q-item v-for="(input,index) in localInput" :key="index">
<q-item-section side>
<q-icon name="mdi-menu-right" />
</q-item-section>
<q-item-section>
{{input}}
</q-item-section>
</q-item>
</q-list>

<q-select
v-if="editMode"
style="width: 100%;"
dense
:options="extraInput"
use-input
outlined
use-chips
input-debounce="0"
new-value-mode="add"
multiple
v-model="localInput"
@input="signalInput"
@keydown="signalInput"
>
<template v-slot:prepend v-if="inputIcon">
<q-icon :name="inputIcon" />
</template>
</q-select>

<q-separator color="grey q-mt-lg" />

</div>

</template>

<script lang="ts">
import { Component, Emit, Prop, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import { I_ExtraFields } from "src/interfaces/I_Blueprint"
@Component({
components: { }
})
export default class Field_MultiSelect extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({ default: [] }) readonly inputDataValue!: []
@Prop() readonly isNew!: boolean
@Prop() readonly editMode!: boolean
changedInput = false
localInput = []
@Watch("inputDataValue", { deep: true, immediate: true })
reactToInputChanges () {
this.localInput = (this.inputDataValue) ? this.inputDataValue : []
}
get inputIcon () {
return this.inputDataBluePrint?.icon
}
get extraInput () {
// @ts-ignore
return this.inputDataBluePrint?.predefinedSelectValues
}
@Emit()
signalInput () {
console.log("emit")
this.changedInput = true
return this.localInput
}
}
</script>
67 changes: 67 additions & 0 deletions src/components/Field_Number.vue
@@ -0,0 +1,67 @@
<template>
<div>
<div class="flex justify-start items-center text-weight-bolder q-mb-sm q-mt-md">
<q-icon v-if="inputIcon" :name="inputIcon" size="20px" class="q-mr-md" min="1"/>
{{inputDataBluePrint.name}}
</div>

<q-list
v-if="!editMode"
dense>
<q-item>
<q-item-section>
{{localInput}}
</q-item-section>
</q-item>
</q-list>

<q-input
v-if="editMode"
v-model.number="localInput"
type="number"
@keyup="signalInput"
outlined
dense
/>

<q-separator color="grey q-mt-lg" />

</div>

</template>

<script lang="ts">
import { Component, Emit, Prop, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import { I_ExtraFields } from "src/interfaces/I_Blueprint"
@Component({
components: { }
})
export default class Field_Number extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({ default: null }) readonly inputDataValue!: null|number
@Prop() readonly editMode!: boolean
@Prop() readonly isNew!: boolean
changedInput = false
localInput: null|number = null
@Emit()
signalInput () {
this.changedInput = true
return this.localInput
}
get inputIcon () {
return this.inputDataBluePrint?.icon
}
@Watch("inputDataValue", { deep: true, immediate: true })
reactToInputChanges () {
this.localInput = this.inputDataValue
}
}
</script>
190 changes: 190 additions & 0 deletions src/components/Field_SingleRelationship.vue
@@ -0,0 +1,190 @@
<template>
<div>
<div class="flex justify-start items-center text-weight-bolder q-mb-sm q-mt-md">
<q-icon v-if="inputIcon" :name="inputIcon" size="20px" class="q-mr-md"/>
{{inputDataBluePrint.name}}
<q-icon v-if="isOneWayRelationship" name="mdi-arrow-right-bold" size="16px" class="q-ml-md">
<q-tooltip>
This is a one-way relationship. <br> Editing this value <b>will not</b> have effect on the connected document/s.
</q-tooltip>
</q-icon>
<q-icon v-if="!isOneWayRelationship" name="mdi-arrow-left-right-bold" size="16px" class="q-ml-md">
<q-tooltip>
This is a two-way relationship. <br> Editing this value <b>will</b> also effect the connected document/s.
</q-tooltip>
</q-icon>

</div>

<q-list
v-if="!editMode && localInput"
dense>
<q-item
clickable
class="text-primary"
@click="openExistingDocument(localInput)">
<q-item-section>
{{localInput.label}}
</q-item-section>
</q-item>
</q-list>

<div class="flex" v-if="editMode">
<q-select
style="flex-grow: 1;"
dense
:options="filteredInput"
use-input
outlined
input-debounce="0"
v-model="localInput"
@filter="filterSelect"
@input="signalInput"
>
<template v-slot:prepend v-if="inputIcon">
<q-icon :name="inputIcon" />
</template>
<template v-slot:option="{ itemProps, itemEvents, opt }">
<q-item
v-bind="itemProps"
v-on="itemEvents"
>
<q-item-section>
<q-item-label v-html="opt.label" ></q-item-label>
</q-item-section>
<q-tooltip v-if='opt.disable'>
This option is unavailable for selection as it is already paired to another.
</q-tooltip>
</q-item>
</template>
</q-select>

</div>

<q-separator color="grey q-mt-lg" />

</div>

</template>

<script lang="ts">
import { Component, Emit, Prop, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import PouchDB from "pouchdb"
import { I_ShortenedDocument } from "src/interfaces/I_OpenedDocument"
import { I_ExtraFields } from "src/interfaces/I_Blueprint"
import { I_FieldRelationship } from "src/interfaces/I_FieldRelationship"
@Component({
components: { }
})
export default class Field_SingleRelationship extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({ default: "" }) readonly inputDataValue!: I_FieldRelationship
@Prop({ default: "" }) readonly currentId!: ""
@Prop() readonly isNew!: boolean
@Prop() readonly editMode!: boolean
changedInput = false
localInput = "" as unknown as I_FieldRelationship
@Watch("inputDataValue", { deep: true, immediate: true })
reactToInputChanges () {
// @ts-ignore
this.localInput = (this.inputDataValue) ? this.inputDataValue : ""
this.reloadObjectListAndCheckIfValueExists().catch(e => console.log(e))
}
get inputIcon () {
return this.inputDataBluePrint?.icon
}
extraInput: I_FieldRelationship[] = []
filteredInput: I_FieldRelationship[] = []
filterSelect (val: string, update: (e: () => void) => void) {
if (val === "") {
update(() => {
this.filteredInput = this.extraInput
})
return
}
update(() => {
const needle = val.toLowerCase()
this.filteredInput = this.extraInput.filter(v => v.label.toLowerCase().indexOf(needle) > -1)
})
}
@Watch("inputDataBluePrint", { deep: true, immediate: true })
reactToBlueprintChanges () {
this.reloadObjectListAndCheckIfValueExists().catch(e => console.log(e))
}
@Watch("currentId")
reactToIDChanges () {
this.reloadObjectListAndCheckIfValueExists().catch(e => console.log(e))
}
get isOneWayRelationship () {
return (this.inputDataBluePrint.type === "singleToNoneRelationship" || this.inputDataBluePrint.type === "manyToNoneRelationship")
}
async reloadObjectListAndCheckIfValueExists () {
if (this.inputDataBluePrint?.relationshipSettings && this.currentId.length > 0) {
const CurrentObjectDB = new PouchDB(this.inputDataBluePrint.relationshipSettings.connectedObjectType)
const allObjects = (await CurrentObjectDB.allDocs({ include_docs: true })).rows.map((doc) => {
const objectDoc = doc.doc as unknown as I_ShortenedDocument
const pairedField = (this.inputDataBluePrint?.relationshipSettings?.connectedField) || ""
let isDisabled = false
if (pairedField.length > 0) {
const pairedFieldObject = objectDoc.extraFields.find(f => f.id === pairedField)
const pairingType = this.inputDataBluePrint.type
if (
(typeof pairedFieldObject?.value !== "string" && pairedFieldObject?.value !== null && pairingType === "singleToSingleRelationship") ||
(typeof pairedFieldObject?.value !== "string" && pairedFieldObject?.value !== null && pairingType === "singleToSingleRelationship")) {
isDisabled = true
}
}
return {
_id: objectDoc._id,
value: objectDoc._id,
type: objectDoc.type,
disable: isDisabled,
url: `/project/display-content/${objectDoc.type}/${objectDoc._id}`,
label: objectDoc.extraFields.find(e => e.id === "name")?.value,
pairedField: pairedField
}
}) as unknown as I_FieldRelationship[]
const allObjectsWithoutCurrent: I_FieldRelationship[] = allObjects.filter((obj) => obj._id !== this.currentId)
if (this.localInput._id) {
if (!allObjectsWithoutCurrent.find(e => e._id === this.localInput._id)) {
// @ts-ignore
this.localInput = ""
}
}
this.extraInput = allObjectsWithoutCurrent
}
}
@Emit()
signalInput () {
this.changedInput = true
return this.localInput
}
}
</script>
85 changes: 85 additions & 0 deletions src/components/Field_SingleSelect.vue
@@ -0,0 +1,85 @@
<template>
<div>
<div class="flex justify-start items-center text-weight-bolder q-mb-sm q-mt-md">
<q-icon v-if="inputIcon" :name="inputIcon" size="20px" class="q-mr-md"/>
{{inputDataBluePrint.name}}
</div>

<q-list
v-if="!editMode"
dense>
<q-item>
<q-item-section>
{{localInput}}
</q-item-section>
</q-item>
</q-list>

<q-select
v-if="editMode"
style="width: 100%;"
dense
:options="extraInput"
use-input
outlined
input-debounce="0"
new-value-mode="add"
v-model="localInput"
@input="signalInput"
@keydown="signalInput"
>
<template v-slot:prepend v-if="inputIcon">
<q-icon :name="inputIcon" />
</template>
</q-select>

<q-separator color="grey q-mt-lg" />

</div>

</template>

<script lang="ts">
import { Component, Emit, Prop, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import { I_ExtraFields } from "src/interfaces/I_Blueprint"
@Component({
components: { }
})
export default class Field_SingleSelect extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({ default: "" }) readonly inputDataValue!: ""
@Prop() readonly isNew!: boolean
@Prop() readonly editMode!: boolean
changedInput = false
localInput = ""
@Watch("inputDataValue", { deep: true, immediate: true })
reactToInputChanges () {
this.localInput = (this.inputDataValue) ? this.inputDataValue : ""
}
get inputIcon () {
return this.inputDataBluePrint?.icon
}
get extraInput () {
// @ts-ignore
return this.inputDataBluePrint?.predefinedSelectValues
}
@Emit()
signalInput () {
console.log("emit")
this.changedInput = true
return this.localInput
}
}
</script>
75 changes: 75 additions & 0 deletions src/components/Field_Text.vue
@@ -0,0 +1,75 @@
<template>
<div>
<div class="flex justify-start items-center text-weight-bolder q-mb-sm q-mt-md">
<q-icon v-if="inputIcon" :name="inputIcon" size="20px" class="q-mr-md"/>
{{inputDataBluePrint.name}}
</div>

<q-list
v-if="!editMode"
dense>
<q-item>
<q-item-section>
{{localInput}}
</q-item-section>
</q-item>
</q-list>

<q-input
v-if="editMode"
v-model="localInput"
@keyup="signalInput"
outlined
dense
>
<template v-slot:append v-if="isNew && !changedInput">
<q-icon name="close" @click="deletePlaceholder()" class="cursor-pointer" />
</template>
</q-input>

<q-separator color="grey q-mt-lg" />

</div>

</template>

<script lang="ts">
import { Component, Emit, Prop, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import { I_ExtraFields } from "src/interfaces/I_Blueprint"
@Component({
components: { }
})
export default class Field_Text extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({ default: "" }) readonly inputDataValue!: string
@Prop() readonly editMode!: boolean
@Prop() readonly isNew!: boolean
changedInput = false
localInput = ""
deletePlaceholder () {
this.localInput = ""
this.signalInput()
}
@Emit()
signalInput () {
this.changedInput = true
return this.localInput
}
get inputIcon () {
return this.inputDataBluePrint?.icon
}
@Watch("inputDataValue", { deep: true, immediate: true })
reactToInputChanges () {
this.localInput = this.inputDataValue
}
}
</script>
129 changes: 129 additions & 0 deletions src/components/Field_Wysiwyg.vue
@@ -0,0 +1,129 @@
<template>
<div>
<div class="flex justify-start items-center text-weight-bolder q-mb-sm q-mt-md">
<q-icon v-if="inputIcon" :name="inputIcon" size="20px" class="q-mr-md"/>
{{inputDataBluePrint.name}}
</div>

<div v-if="!editMode" v-html="localInput">
</div>

<q-editor
v-model="localInput"
:toolbar="wysiwygOptions"
:fonts="wysiwygFonts"
@input="signalInput"
v-if="editMode"
/>

<q-separator color="grey q-mt-lg" />

</div>

</template>

<script lang="ts">
import { Component, Emit, Prop, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import { I_ExtraFields } from "src/interfaces/I_Blueprint"
@Component({
components: { }
})
export default class Field_Wysiwyg extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({ default: "" }) readonly inputDataValue!: string
@Prop() readonly editMode!: boolean
@Prop() readonly isNew!: boolean
changedInput = false
localInput = ""
@Emit()
signalInput () {
this.changedInput = true
return this.localInput
}
get inputIcon () {
return this.inputDataBluePrint?.icon
}
@Watch("inputDataValue", { deep: true, immediate: true })
reactToInputChanges () {
this.localInput = this.inputDataValue
}
wysiwygFonts = {
arial: "Arial",
arial_black: "Arial Black",
comic_sans: "Comic Sans MS",
courier_new: "Courier New",
impact: "Impact",
lucida_grande: "Lucida Grande",
times_new_roman: "Times New Roman",
verdana: "Verdana"
}
wysiwygOptions = [
["left", "center", "right", "justify"],
["bold", "italic", "underline", "subscript", "superscript"],
[
{
label: this.$q.lang.editor.formatting,
icon: this.$q.iconSet.editor.formatting,
list: "no-icons",
fixedIcon: true,
options: [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"p"
]
},
{
label: this.$q.lang.editor.fontSize,
icon: this.$q.iconSet.editor.fontSize,
fixedIcon: true,
list: "no-icons",
options: [
"size-1",
"size-2",
"size-3",
"size-4",
"size-5",
"size-6",
"size-7"
]
},
{
label: this.$q.lang.editor.defaultFont,
icon: this.$q.iconSet.editor.font,
fixedIcon: true,
list: "no-icons",
options: [
"default_font",
"arial",
"arial_black",
"comic_sans",
"courier_new",
"impact",
"lucida_grande",
"times_new_roman",
"verdana"
]
},
"removeFormat"
],
["hr", "link", "quote", "unordered", "ordered", "outdent", "indent"],
["undo", "redo"],
["fullscreen"],
["viewsource"]
]
}
</script>
267 changes: 267 additions & 0 deletions src/components/ObjectTree.vue
@@ -0,0 +1,267 @@
<template>

<span>
<q-input ref="treeFilter" dark filled v-model="treeFilter" label="Search...">
<template v-slot:prepend>
<q-icon name="mdi-text-search" />
</template>
<template v-slot:append>
<q-icon v-if="treeFilter !== ''" name="clear" class="cursor-pointer" @click="resetTreeFilter" />
</template>
</q-input>

<q-tree
class="q-pa-sm"
:nodes="treeList"
node-key="label"
no-connectors
ref="tree"
dark
:filter="treeFilter"
:selected.sync="selectedTreeNode"
>
<template v-slot:default-header="prop">
<div class="row items-center col-grow">
<q-icon :name="prop.node.icon" class="q-mr-sm" />
<div>{{ prop.node.label }}
<q-badge
class="q-ml-xs"
style="font-size: 12px;"
v-if="prop.node.sticker"
color="primary"
outline
align="top">{{prop.node.sticker}}
<q-tooltip>
Order priority of the document
</q-tooltip>
</q-badge>
</div>
<q-tooltip v-if="prop.node.specialLabel">
Add new {{ prop.node.specialLabel }}
</q-tooltip>
</div>
</template>
</q-tree>

<!--
<q-list>
<q-separator
color="white"
inset
class="q-mt-md"
/>
<q-item
v-ripple
clickable
class="q-mt-md"
>
<q-item-section avatar>
<q-icon :name="menuAddNewItem.icon" />
</q-item-section>
<q-item-section>
{{ menuAddNewItem.label }}
</q-item-section>
</q-item>
</q-list>
-->
</span>

</template>

<script lang="ts">
import { Component, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import { I_ShortenedDocument } from "src/interfaces/I_OpenedDocument"
import { I_NewObjectTrigger } from "src/interfaces/I_NewObjectTrigger"
import PouchDB from "pouchdb"
import { engageBlueprints, retrieveAllBlueprints } from "src/databaseManager/blueprintManager"
import { cleanDatabases } from "src/databaseManager/cleaner"
import { I_Blueprint } from "src/interfaces/I_Blueprint"
const menuAddNewItem = {
icon: "mdi-plus",
label: "Add new object type"
}
@Component({
components: { }
})
export default class ObjectTree extends BaseClass {
menuAddNewItem = menuAddNewItem
treeList: {children: I_ShortenedDocument[], icon: string, label: string}[] = []
firstTimeExpand = true
/**
* A resetter for the currently selected node
*/
selectedTreeNode = null
treeFilter = ""
@Watch("SGET_allBlueprints", { deep: true })
reactToBluePrintRefresh () {
this.buildCurrentObjectTree().catch((e) => { console.log(e) })
}
resetTreeFilter () {
this.treeFilter = ""
// @ts-ignore
const treeFilterDOM = this.$refs.treeFilter as unknown as HTMLInputElement
treeFilterDOM.focus()
}
/**
* Since we are using the object tree as URLs intead of selecting, this resets the select every time a node is clicked
*/
@Watch("selectedTreeNode")
onNodeChange (val: I_NewObjectTrigger) {
if (val !== null) {
this.selectedTreeNode = null
}
}
@Watch("SGET_allOpenedDocuments", { deep: true })
async reactToDocumentListChange () {
await this.buildCurrentObjectTree()
}
sortDocuments (input: I_ShortenedDocument[]) {
input
.sort((a, b) => a.label.localeCompare(b.label))
.sort((a, b) => {
const order1 = a.extraFields.find(e => e.id === "order")?.value
const order2 = b.extraFields.find(e => e.id === "order")?.value
if (order1 < order2) { return 1 }
if (order1 > order2) { return -1 }
return 0
})
input.forEach((e, i) => {
if (e.children.length > 0) { input[i].children = this.sortDocuments(input[i].children) }
})
return input
}
buildTreeHierarchy (input: I_ShortenedDocument[]) {
const map: number[] = []
let node
const roots = []
let i
for (i = 0; i < input.length; i += 1) {
map[input[i]._id] = i // initialize the map
}
for (i = 0; i < input.length; i += 1) {
node = input[i]
if (node.parentDoc !== false) {
// if you have dangling branches check that map[node.parentId] exists
if (input[map[node.parentDoc]]) {
input[map[node.parentDoc]].children.push(node)
} else {
roots.push(node)
}
} else {
roots.push(node)
}
}
const sortedRoots = this.sortDocuments(roots)
return sortedRoots
}
async buildCurrentObjectTree () {
const allBlueprings = this.SGET_allBlueprints
const treeObject: any[] = []
for (const blueprint of allBlueprings) {
const CurrentObjectDB = new PouchDB(blueprint._id)
const allDocuments = await CurrentObjectDB.allDocs({ include_docs: true })
const allDocumentsRows = allDocuments.rows
.map((singleDocument) => {
const doc = singleDocument.doc as unknown as I_ShortenedDocument
const parentDocID = doc.extraFields.find(e => e.id === "parentDoc")?.value as unknown as {_id: string}
return {
label: doc.extraFields.find(e => e.id === "name")?.value,
icon: doc.icon,
sticker: doc.extraFields.find(e => e.id === "order")?.value,
parentDoc: (parentDocID) ? parentDocID._id : false,
handler: this.openExistingDocument,
expandable: true,
type: doc.type,
children: [],
hasEdits: false,
isNew: false,
url: doc.url,
extraFields: (doc?.extraFields) || [],
_id: singleDocument.id
} as I_ShortenedDocument
})
const hierarchicalTreeContent = this.buildTreeHierarchy(allDocumentsRows)
const treeRow = {
label: blueprint.namePlural,
icon: blueprint.icon,
_id: blueprint._id,
handler: this.addNewObjectType,
specialLabel: blueprint.nameSingular.toLowerCase(),
children: [
...hierarchicalTreeContent,
{
label: `Add new ${blueprint.nameSingular.toLowerCase()}`,
icon: "mdi-plus",
handler: this.addNewObjectType,
_id: blueprint._id
}
]
}
treeObject.push(treeRow)
}
this.treeList = treeObject
if (this.firstTimeExpand) {
this.firstTimeExpand = false
await this.$nextTick()
this.$refs.tree.expandAll()
}
}
async created () {
await cleanDatabases()
await this.processBluePrints()
setTimeout(() => {
this.buildCurrentObjectTree().catch((e) => { console.log(e) })
}, 1000)
}
/**
* Processes all blueprints and redies the store for population of the app
*/
async processBluePrints (): Promise<void> {
await engageBlueprints()
const allObjectBlueprints = (await retrieveAllBlueprints()).rows.map((blueprint) => {
return blueprint.doc
}) as I_Blueprint[]
this.SSET_allBlueprints(allObjectBlueprints)
}
}
</script>
121 changes: 121 additions & 0 deletions src/components/TopTabs.vue
@@ -0,0 +1,121 @@
<template>

<transition
enter-active-class="animated slideInDown"
leave-active-class="animated slideOutUp"
appear
:duration="600"
>
<q-header
v-if="localDocuments.length > 0"
elevated
class="bg-dark text-cultured"
>
<q-dialog
v-if="currentlyCheckedDocument"
v-model="documentCloseDialogConfirm"
persistent>
<q-card>
<q-card-section class="row items-center">
<span class="q-ml-sm">Discard changes to {{retrieveFieldValue(currentlyCheckedDocument,'name')}}?</span>
</q-card-section>

<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn
flat
label="Discard changes"
color="primary"
v-close-popup
@click="closeDocument(currentlyCheckedDocument)" />
</q-card-actions>
</q-card>
</q-dialog>

<q-tabs
align="left"
inline-label
no-caps>
<transition-group
name="list"
tag="div"
class="headerTransitionWrapper"
enter-active-class="animated fadeIn"
leave-active-class="animated fadeOut"
appear
:duration="300">
<q-route-tab
:ripple="false"
v-for="document in localDocuments"
:to="`/project/display-content/${document.type}/${document._id}`"
:key="document.type+document._id"
:icon="document.icon"
:label="retrieveFieldValue(document,'name')"
:alert="document.hasEdits"
alert-icon="mdi-feather"
>
<q-btn
round
dense
class="z-max q-ml-sm"
:class="{'q-mr-sm': document.hasEdits}"
size="xs"
icon="close"
@click.stop.prevent="checkForCloseOpenedDocument(document)"
/>
</q-route-tab>
</transition-group>
</q-tabs>

</q-header>
</transition>

</template>

<script lang="ts">
import { Component, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import { I_OpenedDocument } from "src/interfaces/I_OpenedDocument"
@Component({
components: { }
})
export default class TppTabs extends BaseClass {
documentCloseDialogConfirm = false
currentlyCheckedDocument = null as unknown as I_OpenedDocument
checkForCloseOpenedDocument (input: I_OpenedDocument) {
this.currentlyCheckedDocument = input
if (input.hasEdits) {
this.documentCloseDialogConfirm = true
} else {
this.closeDocument(input)
}
}
closeDocument (input: I_OpenedDocument) {
this.SSET_removeOpenedDocument(input)
setTimeout(() => {
this.refreshRoute()
}, 100)
}
@Watch("SGET_allOpenedDocuments", { deep: true })
reactToDocumentListChange (val: {docs: I_OpenedDocument[]}) {
this.localDocuments = []
this.localDocuments = val.docs
this.refreshRoute()
}
localDocuments: I_OpenedDocument[] = []
}
</script>

<style lang="scss" scoped>
.headerTransitionWrapper {
display: flex;
}
</style>
11 changes: 11 additions & 0 deletions src/css/app.scss
@@ -0,0 +1,11 @@
// app global css in SCSS form

@import './app/customColors.scss';

body {
background-color: #fff;
}

.q-drawer-container * {
user-select: none !important;
}
11 changes: 11 additions & 0 deletions src/css/app/customColors.scss
@@ -0,0 +1,11 @@
@import '../quasar.variables.scss';

@each $name, $color in $customColors {
.text-#{$name} {
color: $color;
}

.bg-#{$name} {
background-color: $color;
}
}
32 changes: 32 additions & 0 deletions src/css/quasar.variables.scss
@@ -0,0 +1,32 @@
// Quasar SCSS (& Sass) Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.

// Check documentation for full list of Quasar variables

// Your own variables (that are declared here) and Quasar's own
// ones will be available out of the box in your .vue/.scss/.sass files

// It's highly recommended to change the default colors
// to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website.

$primary : #d2a334;
$secondary : #a4031f;
$accent : #dde4e4;

$dark : #19323c;

$positive : #21ba45;
$negative : #c10015;
$info : #31ccec;
$warning : #f2c037;

$customColors: (
'gunmetal': #19323c,
'ruby-red': #a4031f,
'satin-sheen-gold': #d2a334,
'gainsboro': #dde4e4,
'cultured': #f5f5f5
);
67 changes: 67 additions & 0 deletions src/databaseManager/blueprintManager.ts
@@ -0,0 +1,67 @@
import { locationBlueprint } from "./blueprints/locations"
import { I_Blueprint } from "./../interfaces/I_Blueprint"
import { characterBlueprint } from "./blueprints/characters"
import PouchDB from "pouchdb"
import _ from "lodash"

/**
* Loads all the blueprints and processes them apropriatelly
*/
export const engageBlueprints = async () => {
const BlueprintsDB = new PouchDB("blueprints")

/**
* List of all blueprintes needed to get processed
*/
const allBluePrints: I_Blueprint[] = [
characterBlueprint,
locationBlueprint
]

/**
* Processes all blueprints
*/
for (const newBlueprint of allBluePrints) {
try {
// Try adding a brand new data blueprint
await BlueprintsDB.put(newBlueprint)
} catch (e) {
// Proceed with checking of the contents of the blueprint if it already exists
const currentBlueprint = await BlueprintsDB.get(newBlueprint._id) as I_Blueprint
const hasChanges = checkBlueprintUpdate(newBlueprint, currentBlueprint)

// If there are changes, overwrite the old with new
if (hasChanges) {
newBlueprint._rev = currentBlueprint._rev
await BlueprintsDB.put(newBlueprint)
}
}
}
}

/**
* Checks if there are any changes to the blueprint being processed by comparing it with the old one
* @param newBlueprint The updated blueprint freshly retrieved from the source file
* @param currentBlueprint The current blueprint existing in the database
*/
export const checkBlueprintUpdate = (newBlueprint: I_Blueprint, currentBlueprint: I_Blueprint): boolean => {
let hasChanges = false

// Check for naming and icon changes and compare the extra fields via Lodash
if (
newBlueprint?.namePlural !== currentBlueprint?.namePlural ||
newBlueprint?.nameSingular !== currentBlueprint?.nameSingular ||
newBlueprint?.icon !== currentBlueprint?.icon ||
_.isEqual(newBlueprint.extraFields, currentBlueprint.extraFields) === false
) { hasChanges = true }

return hasChanges
}

/**
* Retrieves all blueprints
*/
export const retrieveAllBlueprints = async () => {
const BlueprintsDB = new PouchDB("blueprints")
return await BlueprintsDB.allDocs({ include_docs: true })
}
136 changes: 136 additions & 0 deletions src/databaseManager/blueprints/characters.ts
@@ -0,0 +1,136 @@
import { I_Blueprint } from "./../../interfaces/I_Blueprint"
export const characterBlueprint: I_Blueprint = {
_id: "characters",
namePlural: "Characters",
nameSingular: "Character",
icon: "mdi-account",
extraFields: [
{
id: "name",
name: "Name",
type: "text",
icon: "mdi-account",
sizing: 6
},
{
id: "parentDoc",
name: "Parent document",
type: "singleToNoneRelationship",
sizing: 4,
relationshipSettings: {
connectedObjectType: "characters"
}
},
{
id: "order",
name: "Order number",
type: "number",
icon: "mdi-file-tree",
sizing: 2
},
{
id: "sex",
name: "Sex",
type: "singleSelect",
icon: "mdi-gender-male-female",
sizing: 3,
predefinedSelectValues: ["Male", "Female", "Other"]
},
{
id: "otherNames",
name: "Other names",
type: "list",
icon: "mdi-account-plus",
sizing: 6
},
{
id: "skills",
name: "Skills",
type: "list",
icon: "mdi-sword-cross",
sizing: 6,
predefinedListExtras: {
affix: "Level",
extraSelectValueList: [
"Trainee",
"Good",
"Expert",
"God!"
]
}
},
{
id: "personalityTraits",
name: "Personality traits",
type: "multiSelect",
icon: "mdi-head-cog",
sizing: 6,
predefinedSelectValues: ["Impulsive", "Clever", "Leet", "Streetsmart", "Bashful", "CRAZY!!!"]
},
{
id: "manyToSingle",
name: "MANY to SINGLE",
type: "manyToSingleRelationship",
sizing: 6,
relationshipSettings: {
connectedObjectType: "characters",
connectedField: "singleToMany"
}
},
{
id: "singleToMany",
name: "SINGLE to MANY",
type: "singleToManyRelationship",
sizing: 6,
relationshipSettings: {
connectedObjectType: "characters",
connectedField: "manyToSingle"
}
},
{
id: "singleToNone",
name: "SINGLE to NONE",
type: "singleToNoneRelationship",
sizing: 6,
relationshipSettings: {
connectedObjectType: "characters"
}
},
{
id: "singleToSingle",
name: "SINGLE to SINGLE",
type: "singleToSingleRelationship",
sizing: 6,
relationshipSettings: {
connectedObjectType: "characters",
connectedField: "singleToSingle"
}
},
{
id: "manyToNone",
name: "MANY to NONE",
type: "manyToNoneRelationship",
sizing: 6,
relationshipSettings: {
connectedObjectType: "characters"
}
},
{
id: "manyToMany",
name: "MANY to MANY",
type: "manyToManyRelationship",
sizing: 6,
relationshipSettings: {
connectedObjectType: "characters",
connectedField: "manyToMany"
}
},
{
id: "dreamAndMemes",
name: "Dreams and memes",
type: "wysiwyg",
sizing: 12
}

]
}
31 changes: 31 additions & 0 deletions src/databaseManager/blueprints/locations.ts
@@ -0,0 +1,31 @@
import { I_Blueprint } from "../../interfaces/I_Blueprint"
export const locationBlueprint: I_Blueprint = {
_id: "locations",
namePlural: "Locations",
nameSingular: "Location",
icon: "mdi-map-marker-radius",
extraFields: [
{
id: "name",
name: "Name",
type: "text",
sizing: 12
},
{
id: "otherNames",
name: "Other names",
type: "list",
sizing: 6
},
{
id: "connectedCharacters",
name: "Connected characters",
type: "singleToManyRelationship",
sizing: 6,
relationshipSettings: {
connectedObjectType: "characters",
connectedField: "connectedLocations"
}
}
]
}
37 changes: 37 additions & 0 deletions src/databaseManager/characters.ts
@@ -0,0 +1,37 @@
import PouchDB from "pouchdb"
import PouchDBFind from "pouchdb-find"

PouchDB.plugin(PouchDBFind)

const CharactersDB = new PouchDB("characters")

export const charactersStartup = async (): Promise<void> => {
// await createCharacter("Sariine")
// await createCharacter("Liarni")

await CharactersDB.createIndex({
index: { fields: ["name", "age"] }
})

const indexes = await CharactersDB.getIndexes()

console.log(indexes)

const foundCharacters = await CharactersDB.find({
selector: { name: "Sariine" },
fields: ["name", "age"],
sort: ["name"]
})
console.log(foundCharacters)
}

export const createCharacter = async (name: string): Promise<void> => {
const newCharacter = {
name: name,
age: 56
}

await CharactersDB.post(
newCharacter
)
}
34 changes: 34 additions & 0 deletions src/databaseManager/cleaner.ts
@@ -0,0 +1,34 @@
import { retrieveAllBlueprints } from "src/databaseManager/blueprintManager"
import { I_Blueprint } from "../interfaces/I_Blueprint"
import PouchDB from "pouchdb"

export const cleanDatabases = async () => {
const allBlueprints = (await retrieveAllBlueprints()).rows.map((blueprint) => {
return blueprint.doc
}) as I_Blueprint[]

allBlueprints.forEach(blueprint => {
let activeDB = new PouchDB(blueprint._id)
const cleanedDB = new PouchDB(`${blueprint._id}Clean`)
let originalTableSize: number
let cleanedTableSize

activeDB.info().then((result) => {
originalTableSize = result.doc_count

activeDB.replicate.to(cleanedDB, { filter: function (doc: {_deleted: boolean}) { if (doc._deleted) { return false } else { return doc } } }).on("complete", function () {
cleanedDB.info().then((cleanedResult) => {
cleanedTableSize = cleanedResult.doc_count
if (cleanedTableSize === originalTableSize) {
activeDB.destroy().then(() => {
activeDB = new PouchDB(blueprint._id)
cleanedDB.replicate.to(activeDB, { filter: function (doc: {_deleted: boolean}) { if (doc._deleted) { return false } else { return doc } } }).on("complete", function () {
cleanedDB.destroy().catch((err) => { console.log(err) })
}).catch((err) => { console.log(err) })
}).catch((err) => { console.log(err) })
}
}).catch((err) => { console.log(err) })
}).catch((err) => { console.log(err) })
}).catch((err) => { console.log(err) })
})
}
210 changes: 210 additions & 0 deletions src/databaseManager/relationshipManager.ts
@@ -0,0 +1,210 @@
import PouchDB from "pouchdb"
import { I_Blueprint } from "src/interfaces/I_Blueprint"
import { I_FieldRelationship } from "src/interfaces/I_FieldRelationship"

import { I_ExtraDocumentFields, I_OpenedDocument } from "./../interfaces/I_OpenedDocument"

export const single_changeRelationshipToAnotherObject = async (
field: I_ExtraDocumentFields,
currentDocument:I_OpenedDocument,
previouDocument: I_OpenedDocument) => {
const currentValue = field.value
const previousValue = (previouDocument?.extraFields?.find(e => e.id === field.id))?.value || ""

const BlueprintsDB = new PouchDB("blueprints")
const currentBlueprint = await BlueprintsDB.get(currentDocument.type) as I_Blueprint
const fieldType = (currentBlueprint.extraFields.find(e => e.id === field.id))?.type

const updatedDocuments:I_OpenedDocument[] = []

if (!previousValue && typeof currentValue !== "string" && currentValue) {
if (fieldType === "singleToSingleRelationship") {
updatedDocuments.push(await single_addRelationShipToAnotherObject(field, currentValue, currentDocument))
}
if (fieldType === "singleToManyRelationship") {
updatedDocuments.push(await many_addRelationShipToAnotherObject(field, currentValue, currentDocument))
}
}

if (previousValue && typeof currentValue !== "string" && currentValue) {
if (fieldType === "singleToSingleRelationship") {
updatedDocuments.push(await single_removeRelationShipFromAnotherObject(previousValue))
updatedDocuments.push(await single_addRelationShipToAnotherObject(field, currentValue, currentDocument))
}
if (fieldType === "singleToManyRelationship") {
const removedValued = await many_removeRelationShipFromAnotherObject(previousValue, currentDocument)
if (removedValued) {
updatedDocuments.push(removedValued)
}
updatedDocuments.push(await many_addRelationShipToAnotherObject(field, currentValue, currentDocument))
}
}

if ((previousValue && typeof currentValue === "string") || (previousValue && !currentValue)) {
if (fieldType === "singleToSingleRelationship") {
updatedDocuments.push(await single_removeRelationShipFromAnotherObject(previousValue))
}
if (fieldType === "singleToManyRelationship") {
const removedValued = await many_removeRelationShipFromAnotherObject(previousValue, currentDocument)
if (removedValued) {
updatedDocuments.push(removedValued)
}
}
}
return updatedDocuments
}

export const single_addRelationShipToAnotherObject = async (
field: I_ExtraDocumentFields,
currentValue: I_FieldRelationship,
currentDocument: I_OpenedDocument
) => {
const typeToFind = currentValue.type
const idToFind = currentValue._id

const PairedObjectDB = new PouchDB(typeToFind)
const pairedDocument = await PairedObjectDB.get(idToFind) as I_OpenedDocument
const pairedField = currentValue.pairedField
const pairedFieldIndex = pairedDocument.extraFields.findIndex(e => e.id === pairedField)

pairedDocument.extraFields[pairedFieldIndex].value = {
_id: currentDocument._id,
value: currentDocument._id,
type: currentDocument.type,
url: `/project/display-content/${currentDocument.type}/${currentDocument._id}`,
label: currentDocument.extraFields.find(e => e.id === "name")?.value,
pairedField: field.id
}

await PairedObjectDB.put(pairedDocument)

return pairedDocument
}

export const single_removeRelationShipFromAnotherObject = async (
previousValue: I_FieldRelationship
) => {
const typeToFind = previousValue.type
const idToFind = previousValue._id

const PairedObjectDB = new PouchDB(typeToFind)
const pairedDocument = await PairedObjectDB.get(idToFind) as I_OpenedDocument
const pairedField = previousValue.pairedField
const pairedFieldIndex = pairedDocument.extraFields.findIndex(e => e.id === pairedField)

pairedDocument.extraFields[pairedFieldIndex].value = ""

await PairedObjectDB.put(pairedDocument)

return pairedDocument
}

export const many_changeRelationshipToAnotherObject = async (
field: I_ExtraDocumentFields,
currentDocument:I_OpenedDocument,
previouDocument: I_OpenedDocument) => {
const currentValue: I_FieldRelationship[] = (field.value && typeof field.value !== "string") ? field.value : []
const previousValue: I_FieldRelationship[] = (previouDocument?.extraFields?.find(e => e.id === field.id))?.value || []

const BlueprintsDB = new PouchDB("blueprints")
const currentBlueprint = await BlueprintsDB.get(currentDocument.type) as I_Blueprint

const fieldType = (currentBlueprint.extraFields.find(e => e.id === field.id))?.type

const addedValues = currentValue.filter(val => !previousValue.find(subVal => subVal._id === val._id))
const removedValues = previousValue.filter(val => !currentValue.find(subVal => subVal._id === val._id))
const updatedDocuments:I_OpenedDocument[] = []

for (const addedValue of addedValues) {
if (fieldType === "manyToManyRelationship") {
updatedDocuments.push(await many_addRelationShipToAnotherObject(field, addedValue, currentDocument))
}

if (fieldType === "manyToSingleRelationship") {
updatedDocuments.push(await single_addRelationShipToAnotherObject(field, addedValue, currentDocument))
}
}

for (const removedValue of removedValues) {
if (fieldType === "manyToManyRelationship") {
const removedValued = await many_removeRelationShipFromAnotherObject(removedValue, currentDocument)
if (removedValued) {
updatedDocuments.push(removedValued)
}
}

if (fieldType === "manyToSingleRelationship") {
const removedValued = await single_removeRelationShipFromAnotherObject(removedValue)
if (removedValued) {
updatedDocuments.push(removedValued)
}
}
}

return updatedDocuments
}

export const many_addRelationShipToAnotherObject = async (
field: I_ExtraDocumentFields,
currentValue: I_FieldRelationship,
currentDocument: I_OpenedDocument
) => {
const typeToFind = currentValue.type
const idToFind = currentValue._id

const PairedObjectDB = new PouchDB(typeToFind)
const pairedDocument = await PairedObjectDB.get(idToFind) as I_OpenedDocument
const pairedField = currentValue.pairedField
const pairedFieldIndex = pairedDocument.extraFields.findIndex(e => e.id === pairedField)

let prairedFieldValue: I_FieldRelationship[] = pairedDocument.extraFields[pairedFieldIndex].value

const newValue = {
_id: currentDocument._id,
value: currentDocument._id,
type: currentDocument.type,
url: `/project/display-content/${currentDocument.type}/${currentDocument._id}`,
label: currentDocument.extraFields.find(e => e.id === "name")?.value,
pairedField: field.id
}

prairedFieldValue = (Array.isArray(prairedFieldValue)) ? prairedFieldValue : []

const valueExistsAlready = (prairedFieldValue.find(e => e._id === newValue._id))

if (!valueExistsAlready) {
prairedFieldValue.push(newValue)
}

pairedDocument.extraFields[pairedFieldIndex].value = prairedFieldValue

await PairedObjectDB.put(pairedDocument)

return pairedDocument
}

export const many_removeRelationShipFromAnotherObject = async (
previousValue: I_FieldRelationship,
currentDocument: I_OpenedDocument
) => {
const typeToFind = previousValue.type
const idToFind = previousValue._id

const PairedObjectDB = new PouchDB(typeToFind)
let pairedDocument = false as unknown as I_OpenedDocument
try { pairedDocument = await PairedObjectDB.get(idToFind) } catch (e) { return pairedDocument }

const pairedField = previousValue.pairedField
const pairedFieldIndex = pairedDocument.extraFields.findIndex(e => e.id === pairedField)

const currentValues: I_FieldRelationship[] = pairedDocument.extraFields[pairedFieldIndex].value

const indexToRemove = currentValues.findIndex(e => e._id === currentDocument._id)

currentValues.splice(indexToRemove, 1)
pairedDocument.extraFields[pairedFieldIndex].value = currentValues

await PairedObjectDB.put(pairedDocument)

return pairedDocument
}
7 changes: 7 additions & 0 deletions src/env.d.ts
@@ -0,0 +1,7 @@
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: string;
VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined;
VUE_ROUTER_BASE: string | undefined;
}
}
7 changes: 7 additions & 0 deletions src/i18n/en-us/index.ts
@@ -0,0 +1,7 @@
// This is just an example,
// so you can safely delete all default props below

export default {
failed: "Action failed",
success: "Action was successful"
}
5 changes: 5 additions & 0 deletions src/i18n/index.ts
@@ -0,0 +1,5 @@
import enUS from "./en-us"

export default {
"en-us": enUS
}
22 changes: 22 additions & 0 deletions src/index.template.html
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title><%= productName %></title>

<meta charset="utf-8">
<meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">

<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="icon" type="image/ico" href="favicon.ico">
</head>
<body>
<!-- DO NOT touch the following DIV -->
<div id="q-app"></div>
</body>
</html>
37 changes: 37 additions & 0 deletions src/interfaces/I_Blueprint.ts
@@ -0,0 +1,37 @@
export interface I_ExtraFields {
id: string
name: string,
icon?: string,
sizing: number
type:
"text" |
"number" |
"list" |
"wysiwyg" |
"singleSelect" |
"multiSelect" |
"singleToNoneRelationship" |
"manyToNoneRelationship" |
"singleToSingleRelationship" |
"singleToManyRelationship" |
"manyToSingleRelationship" |
"manyToManyRelationship"

predefinedListExtras?: {
affix?: string
extraSelectValueList?: string[]
}
predefinedSelectValues?: string[]
relationshipSettings?: {
connectedObjectType: string
connectedField?: string
}
}
export interface I_Blueprint{
_id: string
_rev?: string
namePlural: string
nameSingular: string,
icon: string
extraFields: I_ExtraFields[]
}
8 changes: 8 additions & 0 deletions src/interfaces/I_FieldRelationship.ts
@@ -0,0 +1,8 @@
export interface I_FieldRelationship{
type: string
label: string
value: string
url: string
_id: string
pairedField: string
}
6 changes: 6 additions & 0 deletions src/interfaces/I_NewObjectTrigger.ts
@@ -0,0 +1,6 @@
export interface I_NewObjectTrigger {
label?: string
icon?: string
handler?: (e: I_NewObjectTrigger) => void
_id: string
}