reworked hierarchical tree and keybinds

This commit is contained in:
Elvanos 2021-02-22 23:30:18 +01:00
parent bae4b18028
commit c23cba9f60
35 changed files with 1307 additions and 465 deletions

View file

@ -59,7 +59,7 @@ module.exports = {
globals: {
ga: false, // Google Analytics
cordova: true,
__statics: true,
process: true,
Capacitor: true,
chrome: true
@ -72,7 +72,7 @@ module.exports = {
// allow paren-less arrow functions
'arrow-parens': 'off',
'one-var': 'off',
'brace-style': ["error", "stroustrup"],
'import/first': 'off',
'import/named': 'error',
'import/namespace': 'error',

View file

@ -1,5 +1,20 @@
# Changelog
## 0.1.2
### Bugfixes
### New features
- Reworked hierarchical left tree
- Added "Add under parent" button the hiearachical tree
- Added mouse button and improved keyboard support to the hierarchical tree
### QoL adjustments
- Added middle-click closing for the tabs
- Reversed default custom sorting for "Order" field in the left side tree
## 0.1.1
### Bugfixes
@ -7,6 +22,7 @@
- Fixed a bunch of typos
- Fixed names not changing with single/multi relatinships if one gets name updated showing on the others properly
- Fixed forced lower-case for notes in lists and relashionship fields
- Fixed a bug that prevented documents with the same names properly working in the hierarchical tree
### New features

View file

@ -27,7 +27,6 @@ function createWindow () {
// 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

View file

@ -7,9 +7,40 @@
<script lang="ts">
import BaseClass from "src/BaseClass"
import { Component } from "vue-property-decorator"
import { defaultKeybinds } from "src/appSettings/defaultKeybinds"
@Component
export default class App extends BaseClass {
created () {
window.addEventListener("auxclick", this.reactToMiddleClick)
document.body.onmousedown = function (e) {
if (e.button === 1) {
e.preventDefault()
return false
}
}
this.registerDefaultKeybinds()
}
destroyed () {
window.removeEventListener("auxclick", this.reactToMiddleClick)
this.deregisterDefaultKeybinds()
}
reactToMiddleClick (e: {button: number, preventDefault: ()=> void}) {
if (e.button === 1) {
e.preventDefault()
}
}
registerDefaultKeybinds () {
defaultKeybinds.forEach(e => this.SSET_registerDefaultKeybind(e))
}
deregisterDefaultKeybinds () {
defaultKeybinds.forEach(e => this.SSET_deregisterDefaultKeybind(e))
}
}
</script>

View file

@ -1,3 +1,4 @@
import { KeyManagementInterface } from "./store/module-keybinds/state"
import { I_OpenedDocument } from "./interfaces/I_OpenedDocument"
import { Component, Vue } from "vue-property-decorator"
import { namespace } from "vuex-class"
@ -14,15 +15,120 @@ 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"
import { I_KeyPressObject } from "src/interfaces/I_KeypressObject"
const Blueprints = namespace("blueprintsModule")
const OpenedDocuments = namespace("openedDocumentsModule")
const Keybinds = namespace("keybindsModule")
@Component
export default class BaseClass extends Vue {
/****************************************************************/
// Keybinds management
/****************************************************************/
@Keybinds.Getter("getCurrentKeyBindData") SGET_getCurrentKeyBindData!: KeyManagementInterface
@Keybinds.Mutation("registerDefaultKeybind") SSET_registerDefaultKeybind!: (input: I_KeyPressObject) => void
@Keybinds.Mutation("deregisterDefaultKeybind") SSET_deregisterDefaultKeybind!: (input: I_KeyPressObject) => void
@Keybinds.Mutation("registerUserKeybind") SSET_registerUserKeybind!: (input: I_KeyPressObject) => void
@Keybinds.Mutation("deregisterUserKeybind") SSET_deregisterUserKeybind!: (input: I_KeyPressObject) => void
@Keybinds.Mutation("updatePressedKey") SSET_updatePressedKey!: (input: I_KeyPressObject) => void
/**
* Builds a humanly redable represetation of the keybind in string form
* @param keybindId - Keybind object to build the string out of
*/
retrieveKeybindString (keybind: I_KeyPressObject): string {
let keybindString = ""
if (keybind.ctrlKey) {
keybindString += "CTRL + "
}
if (keybind.altKey) {
keybindString += "ALT + "
}
if (keybind.shiftKey) {
keybindString += "SHIFT + "
}
const keybinds = [37, 38, 39, 40, 9, 32, 13]
if (keybinds.includes(keybind.keyCode)) {
if (keybind.keyCode === 13) {
keybindString += "ENTER"
}
if (keybind.keyCode === 9) {
keybindString += "TAB"
}
if (keybind.keyCode === 32) {
keybindString += "SPACE"
}
if (keybind.keyCode === 37) {
keybindString += "LEFT ARROW"
}
if (keybind.keyCode === 38) {
keybindString += "UP ARROW"
}
if (keybind.keyCode === 39) {
keybindString += "RIGHT ARROW"
}
if (keybind.keyCode === 40) {
keybindString += "DOWN ARROW"
}
}
else {
keybindString += String.fromCharCode(keybind.keyCode)
}
if (keybind.note) {
keybindString += `<div class="text-italic keybindNote">${keybind.note}</div>`
}
return keybindString
}
/**
* Determines if the keybind triggered the proper condition
* @param keybindId - ID of the keybind to determine what to match
*/
determineKeyBind (keybindId: string): boolean {
const currentKeybindData = this.SGET_getCurrentKeyBindData
const currentKeyPress = currentKeybindData.currentKeyPress
const pairedDefaultKeybind = currentKeybindData.defaults.find(e => e.id === keybindId)
const pairedCustomKeybind = currentKeybindData.userKeybinds.find(e => e.id === keybindId)
// Kill the script if no keybind exists of this anywhere
if (!pairedDefaultKeybind) {
return false
}
const fieldToCheck = (pairedCustomKeybind) || pairedDefaultKeybind
if (
currentKeyPress.altKey === fieldToCheck.altKey &&
currentKeyPress.ctrlKey === fieldToCheck.ctrlKey &&
currentKeyPress.shiftKey === fieldToCheck.shiftKey &&
currentKeyPress.keyCode === fieldToCheck.keyCode
) {
return true
}
else {
return false
}
}
/****************************************************************/
// Project management
/****************************************************************/
/**
* Creates a brand new project and deleted any present data avaiable right now
* @param projectName The name of the new project
*/
async createNewProject (projectName: string) {
await this.removeCurrentProject()
@ -39,18 +145,22 @@ export default class BaseClass extends Vue {
})
}
/**
* Open an file dialog asking the use for location where to export the project
* @param projectName The name of the project to export
*/
exportProject (projectName: string) {
/*eslint-disable */
remote.dialog.showOpenDialog({
properties: ["openDirectory"]
}).then(async (result) => {
/*eslint-disable */
const folderPath = result.filePaths[0]
PouchDB.plugin(replicationStream.plugin)
//@ts-ignore
// @ts-ignore
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)
//@ts-ignore
// @ts-ignore
const allDBS = await indexedDB.databases()
const DBnames: string[] = allDBS.map((db: {name: string}) => {
@ -65,18 +175,21 @@ export default class BaseClass extends Vue {
}
const ws = fs.createWriteStream(`${folderPath}/${projectName}/${db}.txt`)
//@ts-ignore
// @ts-ignore
await CurrentDB.dump(ws)
}
/* eslint-enable */
}).catch(err => {
console.log(err)
})
/* eslint-enable */
}
/**
* Delete the current project and all its data
*/
async removeCurrentProject () {
/*eslint-disable */
//@ts-ignore
// @ts-ignore
const allDBS = await indexedDB.databases()
const DBnames: string[] = allDBS.map((db: {name: string}) => {
@ -140,10 +253,17 @@ export default class BaseClass extends Vue {
@Blueprints.Mutation("setAllBlueprints") SSET_allBlueprints!: (input: I_Blueprint[]) => void
@Blueprints.Mutation("setBlueprint") SSET_blueprint!: (input: I_Blueprint) => void
addNewObjectType (e: I_NewObjectTrigger) {
// console.log(e.id)
/**
* Generates a brand new route for the new object with individual ID
* @param newObject A new object to be creared
*/
addNewObjectRoute (newObject: I_NewObjectTrigger) {
const parentID = (newObject?.parent) || ""
this.$router.push({ path: `/project/display-content/${e._id}/${uid()}` }).catch((e: {name: string}) => {
this.$router.push({
path: `/project/display-content/${newObject._id}/${uid()}`,
query: { parent: parentID }
}).catch((e: {name: string}) => {
const errorName : string = e.name
if (errorName === "NavigationDuplicated") {
return
@ -152,8 +272,12 @@ export default class BaseClass extends Vue {
})
}
openExistingDocument (e:I_OpenedDocument | I_FieldRelationship) {
this.$router.push({ path: e.url }).catch((e: {name: string}) => {
/**
* Open a new route for an already existing object
* @param existingObject An already existing object passed in
*/
openExistingDocumentRoute (existingObject:I_OpenedDocument | I_FieldRelationship) {
this.$router.push({ path: existingObject.url }).catch((e: {name: string}) => {
const errorName : string = e.name
if (errorName === "NavigationDuplicated") {
return
@ -173,43 +297,74 @@ export default class BaseClass extends Vue {
@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 }
/**
* Retrieves value of requested field. If the field doesn't exist, returns false instead
* @param document - Document object that is expected to contain the field
* @param fieldID - ID of the field to check
*/
retrieveFieldValue (document: I_OpenedDocument, fieldID: string) : string | number | [] | false | I_FieldRelationship {
const fieldData = document?.extraFields
// Fizzle if field doesnt exist
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 */
/**
* Retrieves array length of requested field. If the field doesn't exist or isn't array, returns false instead
* @param document - Document object that is expected to contain the field
* @param fieldID - ID of the field to check
*/
retrieveFieldLength (document: I_OpenedDocument, fieldID: string) : number | false {
const fieldData = document?.extraFields
// Fizzle if field doesnt exist
if (!fieldData) {
return false
}
const fieldValue = fieldData.find(f => f.id === fieldID)?.value
// Fizzle if the value isn't an array
if (!Array.isArray(fieldValue)) {
return false
}
return fieldValue.length
}
/**
* Refreshes the route
*/
refreshRoute () {
const remainingDocuments = this.SGET_allOpenedDocuments.docs
// Assuming there are any documents in the current list
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 }
if (existingDocument) {
return
}
// Load a new route if the new route isnt the one we are already on
const newRoute = `/project/display-content/${lastDocument.type}/${lastDocument._id}`
if (currentRoute !== newRoute) {
this.$router.push({ path: newRoute }).catch(e => console.log(e))
}
} else {
}
// If there are no documents inthe list, just navigate to the front page of the project
else {
this.$router.push({ path: "/project" }).catch((e: {name: string}) => {
if (e && e.name !== "NavigationDuplicated") { console.log(e) }
if (e && e.name !== "NavigationDuplicated") {
console.log(e)
}
}
)
}

View file

@ -0,0 +1,171 @@
export const defaultKeybinds = [
// Open keybind cheatsheet - CTRL + ALT + K
{
altKey: true,
ctrlKey: true,
shiftKey: false,
keyCode: 75,
editable: true,
id: "openKeybindsCheatsheet",
tooltip: "Open keybind cheatsheet"
},
// Quick new document - CTRL + N
{
altKey: false,
ctrlKey: true,
shiftKey: false,
keyCode: 78,
editable: true,
id: "quickNewDocument",
tooltip: "Quick-add new document"
},
// Quick existing document - CTRL + Q
{
altKey: false,
ctrlKey: true,
shiftKey: false,
keyCode: 81,
editable: true,
id: "quickExistingDocument",
tooltip: "Quick-search existing document"
},
// Focus left tree search - CTRL + SHIFT + Q
{
altKey: false,
ctrlKey: true,
shiftKey: true,
keyCode: 81,
editable: true,
id: "focusHierarchicalTree",
tooltip: "Focus search field in the left hierarchical tree"
},
// Clear input in the left tree search - CTRL + SHIFT + W
{
altKey: false,
ctrlKey: true,
shiftKey: true,
keyCode: 87,
editable: true,
id: "clearInputHierarchicalTree",
tooltip: "Clears any input in the search field in the left hierarchical tree"
},
// Close tab - CTRL + W
{
altKey: false,
ctrlKey: true,
shiftKey: false,
keyCode: 87,
editable: true,
id: "closeTab",
tooltip: "Close active document"
},
// Next tab - ALT + RIGHT ARROW
{
altKey: true,
ctrlKey: false,
shiftKey: false,
keyCode: 39,
editable: true,
id: "nextTab",
tooltip: "Next tab"
},
// Previous tab - ALT + LEFT ARROW
{
altKey: true,
ctrlKey: false,
shiftKey: false,
keyCode: 37,
editable: true,
id: "previousTab",
tooltip: "Previous tab"
},
// Save document - CTRL + S
{
altKey: false,
ctrlKey: true,
shiftKey: false,
keyCode: 83,
editable: true,
id: "saveDocument",
tooltip: "Save active document"
},
// Edit document - CTRL + E
{
altKey: false,
ctrlKey: true,
shiftKey: false,
keyCode: 69,
editable: true,
id: "editDocument",
tooltip: "Edit active document"
},
// Delete document - CTRL + D
{
altKey: false,
ctrlKey: true,
shiftKey: false,
keyCode: 68,
editable: true,
id: "deleteDocument",
tooltip: "Delete active document"
},
// Next focus - Tab
{
altKey: false,
ctrlKey: false,
shiftKey: false,
keyCode: 9,
editable: false,
id: "nextFocus",
tooltip: "Focuses next input field/input element/hierarchical tree node",
note: "(functionality is the same as when using a web-browser)"
},
// Previous focus - Shift + Tab
{
altKey: false,
ctrlKey: false,
shiftKey: true,
keyCode: 9,
editable: false,
id: "previousFocus",
tooltip: "Focuses previous input field/input element/hierarchical tree node",
note: "(functionality is the same as when using a web-browser)"
},
// Open document coresponding to the tre node - Enter
{
altKey: false,
ctrlKey: false,
shiftKey: false,
keyCode: 13,
editable: false,
id: "openTreeNode",
tooltip: "Open the focused document in the left hierarchical tree",
note: "(while the hierarchical tree item is focused)"
},
// Collapse/Expand hiararchical tree node - Space
{
altKey: false,
ctrlKey: false,
shiftKey: false,
keyCode: 32,
editable: false,
id: "collapseExpandeTreeNode",
tooltip: "Collapse or open the focused category in the left hierarchical tree",
note: "(while the hierarchical tree item is focused)"
}
]

View file

@ -12,7 +12,7 @@
<q-tree
class="objectTree q-pa-sm"
:nodes="treeList"
:nodes="hierarchicalTree"
node-key="key"
no-connectors
ref="tree"
@ -21,16 +21,19 @@
:selected.sync="selectedTreeNode"
>
<template v-slot:default-header="prop">
<div class="row items-center col-grow">
<div class="row items-center col-grow" @click.stop.prevent="processNodeClick(prop.node)">
<q-icon
:style="`color: ${prop.node.color}; width: 22px !important;`"
:size="(prop.node.icon.includes('fas')? '16px': '21px')"
:name="prop.node.icon"
class="q-mr-sm" />
<div class="documentLabel" :style="`color: ${prop.node.color}`">
<div class="documentLabel"
:style="`color: ${prop.node.color}`"
@click.stop.prevent.middle="processNodeLabelMiddleClick(prop.node)"
>
{{ prop.node.label }}
<span
class="text-primary text-weight-medium"
class="text-primary text-weight-medium q-ml-xs"
v-if="prop.node.isRoot">
({{prop.node.documentCount}})
</span>
@ -45,11 +48,41 @@
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 class="treeButtonGroup">
<q-btn
tabindex="-1"
v-if="!prop.node.specialLabel || prop.node.isRoot"
round
dense
color="primary"
class="z-1 q-ml-sm treeButton treeButton--add"
icon="mdi-plus"
size="8px"
@click.stop.prevent="processNodeNewDocumentButton(prop.node)"
>
<q-tooltip>
Add a new document belonging under {{ prop.node.label }}
</q-tooltip>
</q-btn>
<q-btn
tabindex="-1"
v-if="prop.node.children && prop.node.children.length > 0 && !prop.node.isRoot"
round
dense
color="primary"
class="z-1 q-ml-sm treeButton treeButton--edit"
icon="mdi-pencil"
size="8px"
@click.stop.prevent="openExistingDocumentRoute(prop.node)"
>
<q-tooltip>
Open/Edit {{ prop.node.label }}
</q-tooltip>
</q-btn>
</div>
</div>
</div>
</template>
</q-tree>
@ -81,68 +114,87 @@
</template>
<script lang="ts">
import { Component, Watch, Prop } from "vue-property-decorator"
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 { I_KeyPressObject } from "src/interfaces/I_KeypressObject"
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 {
@Prop() readonly pushedKey!: I_KeyPressObject
@Watch("pushedKey", { deep: true })
processKeyPress (keypress: I_KeyPressObject) {
// Focus left tree search - CTRL + SHIFT + Q
if (keypress.shiftKey && keypress.ctrlKey && !keypress.altKey && keypress.keyCode === 81) {
// @ts-ignore
/****************************************************************/
// KEYBINDS MANAGEMENT
/****************************************************************/
@Watch("SGET_getCurrentKeyBindData", { deep: true })
processKeyPush () {
// Focus left tree search
if (this.determineKeyBind("focusHierarchicalTree")) {
const treeFilterDOM = this.$refs.treeFilter as unknown as HTMLInputElement
treeFilterDOM.focus()
}
// Clear input in the left tree search - CTRL + SHIFT + W
if (keypress.shiftKey && keypress.ctrlKey && !keypress.altKey && keypress.keyCode === 87) {
// Clear input in the left tree search
if (this.determineKeyBind("clearInputHierarchicalTree")) {
this.resetTreeFilter()
}
}
menuAddNewItem = menuAddNewItem
treeList: {children: I_ShortenedDocument[], icon: string, label: string}[] = []
firstTimeExpand = true
/****************************************************************/
// GENERIC FUNCTIONALITY
/****************************************************************/
/**
* A resetter for the currently selected node
* Load all blueprints and build the tree out of them
*/
selectedTreeNode = null
async created () {
// await cleanDatabases()
await this.processBluePrints()
treeFilter = ""
// Unfuck the rendering by giving the app some time to load first
setTimeout(() => {
this.buildCurrentObjectTree().catch((e) => {
console.log(e)
})
}, 500)
}
/****************************************************************/
// BLUEPRINT MANAGEMENT
/****************************************************************/
/**
* In case any of the blueprints change, reload the whole tree
*/
@Watch("SGET_allBlueprints", { deep: true })
reactToBluePrintRefresh () {
this.buildCurrentObjectTree().catch((e) => { console.log(e) })
this.buildCurrentObjectTree().catch((e) => {
console.log(e)
})
}
resetTreeFilter () {
this.treeFilter = ""
// @ts-ignore
const treeFilterDOM = this.$refs.treeFilter as unknown as HTMLInputElement
treeFilterDOM.focus()
/**
* 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)
}
/****************************************************************/
// HIERARCHICAL TREE - HELPERS AND MODELS
/****************************************************************/
/**
* Since we are using the object tree as URLs intead of selecting, this resets the select every time a node is clicked
*/
@ -153,31 +205,94 @@ export default class ObjectTree extends BaseClass {
}
}
/**
*
*/
@Watch("SGET_allOpenedDocuments", { deep: true })
async reactToDocumentListChange () {
await this.buildCurrentObjectTree()
}
/**
* Generic wrapper for adding of new object types to the tree
*/
menuAddNewItem = {
icon: "mdi-plus",
label: "Add new object type"
}
/**
* Contains all the data for the render in tree
*/
hierarchicalTree: {children: I_ShortenedDocument[], icon: string, label: string}[] = []
/**
* Determines if the tree should expand fully at first or not
*/
firstTimeExpand = true
/**
* A resetter for the currently selected node
*/
selectedTreeNode = null
/**
* Filter model for the tree
*/
treeFilter = ""
/**
* Resets the tree filter and refocuses the search box
*/
resetTreeFilter () {
this.treeFilter = ""
const treeFilterDOM = this.$refs.treeFilter as unknown as HTMLInputElement
treeFilterDOM.focus()
}
/****************************************************************/
// HIERARCHICAL TREE - CONTENT CONSTRUCTION
/****************************************************************/
/**
* Sort the whole tree via alphabetical and custom numeric order
* @param input Hierartchical tree object to sort
*/
sortDocuments (input: I_ShortenedDocument[]) {
input
// Sort by name
.sort((a, b) => a.label.localeCompare(b.label))
// Sort by custom order
.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 }
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) }
// Run recursive if the node has any children
if (e.children.length > 0) {
input[i].children = this.sortDocuments(input[i].children)
}
})
return input
}
/**
* Builds proper hiearachy for flat array of documents
* @param input Non-hierarchical tree to build the hiearachy out of
*/
buildTreeHierarchy (input: I_ShortenedDocument[]) {
const map: number[] = []
let node
@ -185,19 +300,22 @@ export default class ObjectTree extends BaseClass {
let i
for (i = 0; i < input.length; i += 1) {
map[input[i]._id] = i // initialize the map
// Initialize the map
map[input[i]._id] = i
}
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 there are any dangling branches check that map[node.parentDoc] exists
if (input[map[node.parentDoc]]) {
input[map[node.parentDoc]].children.push(node)
} else {
}
else {
roots.push(node)
}
} else {
}
else {
roots.push(node)
}
}
@ -207,10 +325,14 @@ export default class ObjectTree extends BaseClass {
return sortedRoots
}
/**
* Builds a brand new sparkling hearchy tree out of available data
*/
async buildCurrentObjectTree () {
const allBlueprings = this.SGET_allBlueprints
const treeObject: any[] = []
// Process all documents, build hieararchy out of the and sort them via name and custom order
for (const blueprint of allBlueprings) {
const CurrentObjectDB = new PouchDB(blueprint._id)
@ -229,7 +351,7 @@ export default class ObjectTree extends BaseClass {
icon: (isCategory) ? "fas fa-folder-open" : doc.icon,
sticker: doc.extraFields.find(e => e.id === "order")?.value,
parentDoc: (parentDocID) ? parentDocID._id : false,
handler: this.openExistingDocument,
handler: this.openExistingDocumentRoute,
expandable: true,
color: color,
type: doc.type,
@ -253,7 +375,7 @@ export default class ObjectTree extends BaseClass {
order: blueprint.order,
_id: blueprint._id,
key: blueprint._id,
handler: this.addNewObjectType,
handler: this.addNewObjectRoute,
specialLabel: blueprint.nameSingular.toLowerCase(),
isRoot: true,
documentCount: documentCount,
@ -262,8 +384,12 @@ export default class ObjectTree extends BaseClass {
{
label: `Add new ${blueprint.nameSingular.toLowerCase()}`,
icon: "mdi-plus",
handler: this.addNewObjectType,
_id: blueprint._id
handler: this.addNewObjectRoute,
children: false,
key: `${blueprint._id}_add`,
_id: blueprint._id,
specialLabel: blueprint.nameSingular.toLowerCase()
}
]
}
@ -271,6 +397,7 @@ export default class ObjectTree extends BaseClass {
treeObject.push(treeRow)
}
// Sort the top level of the blueprints
treeObject.sort((a, b) => {
if (a.order < b.order) {
return 1
@ -282,36 +409,89 @@ export default class ObjectTree extends BaseClass {
return 0
})
this.treeList = treeObject
// Assign the finished object to the render model
this.hierarchicalTree = treeObject
// Expand all on first load
if (this.firstTimeExpand) {
this.firstTimeExpand = false
// await this.$nextTick()
// this.$refs.tree.expandAll()
}
console.log(treeObject)
}
async created () {
// await cleanDatabases()
await this.processBluePrints()
setTimeout(() => {
this.buildCurrentObjectTree().catch((e) => { console.log(e) })
}, 1000)
processNodeNewDocumentButton (node: {
key: string
_id: string
children: []
type: string
isRoot: boolean
specialLabel: string|boolean
}) {
// If this is top level blueprint
if (node.isRoot) {
// @ts-ignore
this.addNewObjectRoute(node)
}
// If this is a custom document
else {
const routeObject = {
_id: node.type,
parent: node._id
}
// @ts-ignore
this.addNewObjectRoute(routeObject)
}
}
/**
* Processes all blueprints and redies the store for population of the app
*/
async processBluePrints (): Promise<void> {
await engageBlueprints()
processNodeLabelMiddleClick (node: {
key: string
_id: string
children: []
type: string
isRoot: boolean
specialLabel: string|boolean
}) {
this.selectedTreeNode = null
const allObjectBlueprints = (await retrieveAllBlueprints()).rows.map((blueprint) => {
return blueprint.doc
}) as I_Blueprint[]
if (!node.specialLabel && !node.isRoot) {
// @ts-ignore
this.openExistingDocumentRoute(node)
}
else {
this.addNewObjectRoute(node)
}
}
this.SSET_allBlueprints(allObjectBlueprints)
processNodeClick (node: {
key: string
children: []
specialLabel: string|boolean
}) {
// If this is a category or has children
if (node.children.length > 0) {
this.expandeCollapseNode(node)
}
// If this lacks a "special label" - AKA anything that isn't the "Add new XY" node
else if (!node.specialLabel) {
// @ts-ignore
this.openExistingDocumentRoute(node)
}
// If this lacks a "special label" - AKA if this is the "Add new XY" node
else {
// @ts-ignore
this.addNewObjectRoute(node)
}
}
expandeCollapseNode (node: {key: string}) {
const treeDOM = this.$refs.tree as unknown as {
setExpanded: (key:string, state: boolean)=> void,
isExpanded: (key:string)=> boolean
}
const isExpanded = treeDOM.isExpanded(node.key)
treeDOM.setExpanded(node.key, !isExpanded)
}
}
</script>
@ -319,14 +499,53 @@ export default class ObjectTree extends BaseClass {
<style lang="scss">
.objectTree {
.q-tree__arrow {
width: 28px;
height: 28px;
margin: 0;
margin-right: 0;
padding: 4px;
}
.q-tree__node-header {
padding: 0;
}
.documentLabel {
max-width: calc(100% - 30px);
width: 100%;
display: flex;
justify-content: space-between;
padding: 4px 4px 4px 0;
}
.treeButtonGroup {
flex-grow: 0;
flex-shrink: 0;
display: flex;
height: fit-content;
margin-left: auto;
align-self: center;
}
}
.treeButton {
opacity: 0.8;
&:hover {
opacity: 1;
}
&--add {
.q-icon {
font-size: 20px;
}
}
&--edit {
.q-icon {
font-size: 14px;
}
}
.q-icon {
color: $dark;
}
}
</style>

View file

@ -11,28 +11,28 @@
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-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-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
<q-tabs
align="left"
inline-label
class="tabsWrapper"
@ -45,7 +45,8 @@
leave-active-class="animated fadeOut"
appear
:duration="300">
<q-route-tab
<q-route-tab
:ripple="false"
v-for="document in localDocuments"
:to="`/project/display-content/${document.type}/${document._id}`"
@ -55,6 +56,7 @@
:style="`color: ${retrieveFieldValue(document,'documentColor')}`"
:alert="document.hasEdits"
alert-icon="mdi-feather"
@click.prevent.middle="checkForCloseOpenedDocument(document)"
>
<q-btn
round
@ -67,6 +69,7 @@
@click.stop.prevent="checkForCloseOpenedDocument(document)"
/>
</q-route-tab>
</transition-group>
</q-tabs>
@ -95,7 +98,8 @@ export default class TppTabs extends BaseClass {
this.currentlyCheckedDocument = input
if (input.hasEdits) {
this.documentCloseDialogConfirm = true
} else {
}
else {
this.closeDocument(input)
}
}
@ -120,56 +124,83 @@ export default class TppTabs extends BaseClass {
@Prop() readonly pushedKey!: I_KeyPressObject
@Watch("pushedKey", { deep: true })
processKeyPress (keypress: I_KeyPressObject) {
// Delete dialog - CTRL + W
if (!keypress.shiftKey && keypress.ctrlKey && !keypress.altKey && keypress.keyCode === 87 && this.localDocuments.length > 0) {
const matchingDocument = this.localDocuments.find(e => e.url === this.$route.path)
/****************************************************************/
// Keybind handling
/****************************************************************/
if (matchingDocument) {
this.checkForCloseOpenedDocument(matchingDocument)
}
/**
* React to keypresses passed from the parent document
*/
@Watch("SGET_getCurrentKeyBindData", { deep: true })
processKeyPush () {
// Delete dialog
if (this.determineKeyBind("closeTab") && this.localDocuments.length > 0) {
this.closeTab()
}
// Next tab - ALT + RIGHT ARROW
if (!keypress.shiftKey && !keypress.ctrlKey && keypress.altKey && keypress.keyCode === 39 && this.localDocuments.length > 0) {
let index = -1
const matchingDocument = this.localDocuments.find((e, i) => {
index = i
return e.url === this.$route.path
})
if (matchingDocument && index !== this.localDocuments.length - 1) {
this.$router.push({ path: this.localDocuments[index + 1].url }).catch((e: {name: string}) => {
if (e && e.name !== "NavigationDuplicated") { console.log(e) }
})
}
if (matchingDocument && index === this.localDocuments.length - 1) {
this.$router.push({ path: this.localDocuments[0].url }).catch((e: {name: string}) => {
if (e && e.name !== "NavigationDuplicated") { console.log(e) }
})
}
// Next tab
if (this.determineKeyBind("nextTab") && this.localDocuments.length > 0) {
this.goToNextTab()
}
// Previous tab - ALT + LEFT ARROW
if (!keypress.shiftKey && !keypress.ctrlKey && keypress.altKey && keypress.keyCode === 37 && this.localDocuments.length > 0) {
let index = -1
const matchingDocument = this.localDocuments.find((e, i) => {
index = i
return e.url === this.$route.path
// Previous tab
if (this.determineKeyBind("previousTab") && this.localDocuments.length > 0) {
this.goToPreviousTab()
}
}
closeTab () {
const matchingDocument = this.localDocuments.find(e => e.url === this.$route.path)
if (matchingDocument) {
this.checkForCloseOpenedDocument(matchingDocument)
}
}
goToNextTab () {
let index = -1
const matchingDocument = this.localDocuments.find((e, i) => {
index = i
return e.url === this.$route.path
})
if (matchingDocument && index !== this.localDocuments.length - 1) {
this.$router.push({ path: this.localDocuments[index + 1].url }).catch((e: {name: string}) => {
if (e && e.name !== "NavigationDuplicated") {
console.log(e)
}
})
}
if (matchingDocument && index === this.localDocuments.length - 1) {
this.$router.push({ path: this.localDocuments[0].url }).catch((e: {name: string}) => {
if (e && e.name !== "NavigationDuplicated") {
console.log(e)
}
})
}
}
if (matchingDocument && index !== 0) {
this.$router.push({ path: this.localDocuments[index - 1].url }).catch((e: {name: string}) => {
if (e && e.name !== "NavigationDuplicated") { console.log(e) }
})
}
goToPreviousTab () {
let index = -1
const matchingDocument = this.localDocuments.find((e, i) => {
index = i
return e.url === this.$route.path
})
if (matchingDocument && index === 0) {
this.$router.push({ path: this.localDocuments[this.localDocuments.length - 1].url }).catch((e: {name: string}) => {
if (e && e.name !== "NavigationDuplicated") { console.log(e) }
})
}
if (matchingDocument && index !== 0) {
this.$router.push({ path: this.localDocuments[index - 1].url }).catch((e: {name: string}) => {
if (e && e.name !== "NavigationDuplicated") {
console.log(e)
}
})
}
if (matchingDocument && index === 0) {
this.$router.push({ path: this.localDocuments[this.localDocuments.length - 1].url }).catch((e: {name: string}) => {
if (e && e.name !== "NavigationDuplicated") {
console.log(e)
}
})
}
}
}

View file

@ -100,7 +100,11 @@ import { I_ExtraFields } from "src/interfaces/I_Blueprint"
export default class Field_List extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({ default: () => { return [] } }) readonly inputDataValue!: {
@Prop({
default: () => {
return []
}
}) readonly inputDataValue!: {
value: string
affix?: string
}[]

View file

@ -30,7 +30,7 @@
:key="single._id"
clickable
class="text-primary"
@click="openExistingDocument(single)">
@click="openExistingDocumentRoute(single)">
<q-item-section>
{{single.label}}
<span class="inline-block q-ml-xs text-italic connectionNote">
@ -116,7 +116,11 @@ import { I_FieldRelationship, I_RelationshipPair } from "src/interfaces/I_FieldR
export default class Field_SingleRelationship extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({ default: () => { return [] } }) readonly inputDataValue!: I_RelationshipPair
@Prop({
default: () => {
return []
}
}) readonly inputDataValue!: I_RelationshipPair
@Prop({ default: "" }) readonly currentId!: ""
@ -225,7 +229,8 @@ export default class Field_SingleRelationship extends BaseClass {
if (!allObjectsWithoutCurrent.find(e => e._id === s._id)) {
// @ts-ignore
this.localInput.splice(index, 1)
} else {
}
else {
const matchedFieldContent = allObjectsWithoutCurrent.find(e => e._id === s._id)
if (matchedFieldContent) {

View file

@ -62,7 +62,11 @@ import { I_ExtraFields } from "src/interfaces/I_Blueprint"
export default class Field_MultiSelect extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({ default: [] }) readonly inputDataValue!: []
@Prop({
default: () => {
return []
}
}) readonly inputDataValue!: []
@Prop() readonly isNew!: boolean

View file

@ -27,7 +27,7 @@
<q-item
clickable
class="text-primary"
@click="openExistingDocument(localInput)">
@click="openExistingDocumentRoute(localInput)">
<q-item-section>
{{localInput.label}}
<span class="inline-block q-ml-xs text-italic connectionNote">
@ -227,7 +227,8 @@ export default class Field_SingleRelationship extends BaseClass {
if (!objectsWithoutCurrent.find(e => e._id === this.localInput._id)) {
// @ts-ignore
this.localInput = ""
} else {
}
else {
const matchedFieldContent = objectsWithoutCurrent.find(e => e._id === this.localInput._id)
if (matchedFieldContent) {
this.localInput.label = matchedFieldContent.label

View file

@ -51,7 +51,8 @@ export const engageBlueprints = async () => {
try {
// Try adding a brand new data blueprint
await BlueprintsDB.put(newBlueprint)
} catch (e) {
}
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)
@ -79,7 +80,9 @@ export const checkBlueprintUpdate = (newBlueprint: I_Blueprint, currentBlueprint
newBlueprint?.nameSingular !== currentBlueprint?.nameSingular ||
newBlueprint?.icon !== currentBlueprint?.icon ||
_.isEqual(newBlueprint.extraFields, currentBlueprint.extraFields) === false
) { hasChanges = true }
) {
hasChanges = true
}
return hasChanges
}

View file

@ -16,19 +16,49 @@ export const cleanDatabases = async () => {
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 () {
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) })
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) })
}).catch((err) => {
console.log(err)
})
}).catch((err) => {
console.log(err)
})
}).catch((err) => {
console.log(err)
})
})
}

View file

@ -201,7 +201,12 @@ export const many_removeRelationShipFromAnotherObject = async (
const PairedObjectDB = new PouchDB(typeToFind)
let pairedDocument = false as unknown as I_OpenedDocument
try { pairedDocument = await PairedObjectDB.get(idToFind) } catch (e) { return pairedDocument }
try {
pairedDocument = await PairedObjectDB.get(idToFind)
}
catch (e) {
return pairedDocument
}
const pairedField = previousValue.pairedField
const pairedFieldIndex = pairedDocument.extraFields.findIndex(e => e.id === pairedField)

View file

@ -3,4 +3,8 @@ export interface I_KeyPressObject {
ctrlKey: boolean
shiftKey: boolean
keyCode: number
editable?: boolean
id?: string
tooltip?: string
note? :string
}

View file

@ -1,6 +1,7 @@
export interface I_NewObjectTrigger {
label?: string
icon?: string
parent?: string
handler?: (e: I_NewObjectTrigger) => void
_id: string
}

View file

@ -12,7 +12,7 @@
<h6 class="text-center q-my-sm">Keybind list</h6>
</q-card-section>
<q-card-section>
<q-card-section>
<q-markup-table>
<thead>
<tr>
@ -21,69 +21,9 @@
</tr>
</thead>
<tbody>
<tr>
<td class="text-left">Focuses next input field/input element/hierarchical tree node</td>
<td class="text-left">TAB <br> <div class="text-italic keybindNote">(functionality is the same as when using a web-browser)</div></td>
</tr>
<tr>
<td class="text-left">Focuses previous input field/input element/hierarchical tree node</td>
<td class="text-left">SHFIT + TAB <br> <div class="text-italic keybindNote">(functionality is the same as when using a web-browser)</div></td>
</tr>
<tr>
<td class="text-left">Open the focused document in the left hierarchical tree</td>
<td class="text-left">SHIFT + TAB <br> <div class="text-italic keybindNote">(while the hierarchical tree item is focused)</div></td>
</tr>
<tr>
<td class="text-left">Open keybind cheatsheet</td>
<td class="text-left">CTRL + ALT + K</td>
</tr>
<tr>
<td class="text-left">Focus search field in the left hierarchical tree</td>
<td class="text-left">CTRL + SHIFT + Q</td>
</tr>
<tr>
<td class="text-left">Clears any inpuit in the search field in the left hierarchical tree</td>
<td class="text-left">CTRL + SHIFT + W</td>
</tr>
<tr>
<td class="text-left">Open the focused document in the left hierarchical tree</td>
<td class="text-left">ENTER <br> <div class="text-italic keybindNote">(while the hierarchical tree item is focused)</div></td>
</tr>
<tr>
<td class="text-left">Collapse or open the focused category in the left hierarchical tree</td>
<td class="text-left">SPACE <br> <div class="text-italic keybindNote">(while the hierarchical tree item is focused)</div></td>
</tr>
<tr>
<td class="text-left">Quick-search existing document</td>
<td class="text-left">CTRL + Q</td>
</tr>
<tr>
<td class="text-left">Quick-add new document</td>
<td class="text-left">CTRL + N</td>
</tr>
<tr>
<td class="text-left">Next tab</td>
<td class="text-left">ALT + RIGHT ARROW</td>
</tr>
<tr>
<td class="text-left">Previous tab</td>
<td class="text-left">ALT + LEFT ARROW</td>
</tr>
<tr>
<td class="text-left">Close active document</td>
<td class="text-left">CTRL + W</td>
</tr>
<tr>
<td class="text-left">Delete active document</td>
<td class="text-left">CTRL + D</td>
</tr>
<tr>
<td class="text-left">Edit active document</td>
<td class="text-left">CTRL + E</td>
</tr>
<tr>
<td class="text-left">Save active document</td>
<td class="text-left">CTRL + S</td>
<tr v-for="keybind in SGET_getCurrentKeyBindData.defaults" :key="keybind.id">
<td class="text-left" v-html="keybind.tooltip"/>
<td class="text-left" v-html="retrieveKeybindString(keybind)"/>
</tr>
</tbody>
</q-markup-table>
@ -199,12 +139,11 @@
content-class="bg-dark text-cultured sideWrapper"
v-model="leftDrawerOpen"
side="left"
:width=375
show-if-above
>
<objectTree
:pushed-key="pushedKey"
/>
<objectTree/>
<q-page-sticky position="bottom-right" class="controlButtons">
@ -242,9 +181,7 @@
</q-drawer>
<!-- Header -->
<topTabs
:pushed-key="pushedKey"
/>
<topTabs/>
<!-- Right drawer -->
<q-drawer
@ -261,7 +198,7 @@
appear
:duration="300"
>
<router-view :key="$route.path" :pushed-key="pushedKey" />
<router-view :key="$route.path" />
</transition>
</q-page-container>
@ -270,13 +207,12 @@
<script lang="ts">
import { Component } from "vue-property-decorator"
import { Component, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import PouchDB from "pouchdb"
import objectTree from "src/components/ObjectTree.vue"
import topTabs from "src/components/TopTabs.vue"
import { I_KeyPressObject } from "src/interfaces/I_KeypressObject"
import { I_ShortenedDocument } from "src/interfaces/I_OpenedDocument"
interface NewObjectDocument {
@ -294,7 +230,23 @@ export default class MainLayout extends BaseClass {
leftDrawerOpen = true
rightDrawerOpen = false
pushedKey = {} as I_KeyPressObject
@Watch("SGET_getCurrentKeyBindData", { deep: true })
processKeyPush () {
// Keybind cheatsheet
if (this.determineKeyBind("openKeybindsCheatsheet")) {
this.keyBindsDialog = true
}
// Quick new document
if (this.determineKeyBind("quickNewDocument")) {
this.populateNewObjectDialog()
}
// Quick open existing document
if (this.determineKeyBind("quickExistingDocument")) {
this.populateExistingObjectDialog().catch(e => console.log(e))
}
}
created () {
window.addEventListener("keyup", this.triggerKeyPush)
@ -306,7 +258,9 @@ export default class MainLayout extends BaseClass {
// @ts-ignore
triggerKeyPush (e) {
if (this.newDocumentDialog || this.existingDocumentDialog || this.keyBindsDialog) { return false }
if (this.newDocumentDialog || this.existingDocumentDialog || this.keyBindsDialog) {
return false
}
if (e?.altKey === true || e?.ctrlKey || e?.shiftKey) {
const ouputKeycombo = {
altKey: e.altKey,
@ -314,33 +268,13 @@ export default class MainLayout extends BaseClass {
shiftKey: e.shiftKey,
keyCode: e.keyCode
}
this.pushedKey = ouputKeycombo
this.processKeyPush()
this.SSET_updatePressedKey(ouputKeycombo)
}
}
newDocumentDialog = false
processKeyPush () {
const currentKey = this.pushedKey
// New document - CTRL + ALT + K
if (!currentKey.shiftKey && currentKey.ctrlKey && currentKey.altKey && currentKey.keyCode === 75) {
this.keyBindsDialog = true
}
// New document - CTRL + N
if (!currentKey.shiftKey && currentKey.ctrlKey && !currentKey.altKey && currentKey.keyCode === 78) {
this.populateNewObjectDialog()
}
// Open existing document - CTRL + Q
if (!currentKey.shiftKey && currentKey.ctrlKey && !currentKey.altKey && currentKey.keyCode === 81) {
this.populateExistingObjectDialog().catch(e => console.log(e))
}
}
newObjectList = [] as NewObjectDocument[]
newDocumentModel = null
@ -406,7 +340,7 @@ export default class MainLayout extends BaseClass {
triggerNewInput (e: NewObjectDocument) {
this.newDocumentDialog = false
this.addNewObjectType(e)
this.addNewObjectRoute(e)
this.newDocumentModel = null
}
@ -490,7 +424,7 @@ export default class MainLayout extends BaseClass {
openExistingInput (e: I_ShortenedDocument) {
this.existingDocumentDialog = false
// @ts-ignore
this.openExistingDocument(e)
this.openExistingDocumentRoute(e)
this.existingDocumentModel = null
}

View file

@ -4,167 +4,167 @@
>
<div class="row justify-start q-col-gutter-x-xl">
<q-dialog
v-model="deleteConfirmationDialog"
>
<q-card>
<q-card-section class="row items-center">
<span class="q-ml-sm">Are you sure want to delete <b>{{retrieveFieldValue(currentData,'name')}}</b>? <br> This action can not be reverted and the data will be lost <b>forever</b>.</span>
</q-card-section>
<q-dialog
v-model="deleteConfirmationDialog"
>
<q-card>
<q-card-section class="row items-center">
<span class="q-ml-sm">Are you sure want to delete <b>{{retrieveFieldValue(currentData,'name')}}</b>? <br> This action can not be reverted and the data will be lost <b>forever</b>.</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn
flat
label="Delete"
color="red"
v-close-popup
@click="deleteDocument()" />
</q-card-actions>
</q-card>
</q-dialog>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn
flat
label="Delete"
color="red"
v-close-popup
@click="deleteDocument()" />
</q-card-actions>
</q-card>
</q-dialog>
<div class="col-12 flex justify-end q-mb-xl q-mt-md">
<q-btn
color="primary"
:label="`Save ${bluePrintData.nameSingular}`"
@click="saveDocument"
class="q-mr-xl"
v-if="editMode"
/>
<q-btn
color="primary"
:label="`Edit ${bluePrintData.nameSingular}`"
@click="toggleEditMode"
class="q-mr-xl"
v-if="!editMode"
/>
<q-btn
v-if="!currentData.isNew"
color="red"
:label="`Delete ${bluePrintData.nameSingular}`"
@click="openDeleteDialog"
/>
</div>
<div
:class="`col-${field.sizing} q-mb-md`"
v-for="field in bluePrintData.extraFields"
:key="field.id"
>
<Field_Break
class="inputWrapper break"
v-if="field.type === 'break' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
<div class="col-12 flex justify-end q-mb-lg q-mt-md">
<q-btn
color="primary"
:label="`Save ${bluePrintData.nameSingular}`"
@click="saveDocument"
class="q-mr-xl"
v-if="editMode"
/>
<Field_Text
class="inputWrapper"
v-if="(field.type === 'text' && retrieveFieldValue(currentData,field.id) || field.type === 'text' && retrieveFieldLength(currentData,field.id) === 0) && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
<q-btn
color="primary"
:label="`Edit ${bluePrintData.nameSingular}`"
@click="toggleEditMode"
class="q-mr-xl"
v-if="!editMode"
/>
<Field_Number
class="inputWrapper"
v-if="field.type === 'number' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
<q-btn
v-if="!currentData.isNew"
color="red"
:label="`Delete ${bluePrintData.nameSingular}`"
@click="openDeleteDialog"
/>
</div>
<Field_Switch
class="inputWrapper"
v-if="field.type === 'switch' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
<div
:class="`col-${field.sizing} q-mb-md`"
v-for="field in bluePrintData.extraFields"
:key="field.id"
>
<Field_ColorPicker
class="inputWrapper"
v-if="field.type === 'colorPicker' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_Break
class="inputWrapper break"
v-if="field.type === 'break' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
/>
<Field_List
class="inputWrapper"
v-if="field.type === 'list' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_Text
class="inputWrapper"
v-if="(field.type === 'text' && retrieveFieldValue(currentData,field.id) || field.type === 'text' && retrieveFieldLength(currentData,field.id) === 0) && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_SingleSelect
class="inputWrapper"
v-if="field.type === 'singleSelect' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_Number
class="inputWrapper"
v-if="field.type === 'number' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_MultiSelect
class="inputWrapper"
v-if="field.type === 'multiSelect' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_Switch
class="inputWrapper"
v-if="field.type === 'switch' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_SingleRelationship
class="inputWrapper"
v-if="(field.type === 'singleToNoneRelationship' || field.type === 'singleToSingleRelationship' || field.type === 'singleToManyRelationship') && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
:current-id="currentData._id"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_ColorPicker
class="inputWrapper"
v-if="field.type === 'colorPicker' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_MultiRelationship
class="inputWrapper"
v-if="(field.type === 'manyToNoneRelationship' || field.type ===
'manyToSingleRelationship' || field.type === 'manyToManyRelationship') && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
:current-id="currentData._id"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_List
class="inputWrapper"
v-if="field.type === 'list' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_Wysiwyg
class="inputWrapper"
v-if="field.type === 'wysiwyg' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="(retrieveFieldValue(currentData, field.id)) ? retrieveFieldValue(currentData, field.id) : ''"
:isNew="currentData.isNew"
:editMode="editMode"
:current-id="currentData._id"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_SingleSelect
class="inputWrapper"
v-if="field.type === 'singleSelect' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
</div>
<Field_MultiSelect
class="inputWrapper"
v-if="field.type === 'multiSelect' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_SingleRelationship
class="inputWrapper"
v-if="(field.type === 'singleToNoneRelationship' || field.type === 'singleToSingleRelationship' || field.type === 'singleToManyRelationship') && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
:current-id="currentData._id"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_MultiRelationship
class="inputWrapper"
v-if="(field.type === 'manyToNoneRelationship' || field.type ===
'manyToSingleRelationship' || field.type === 'manyToManyRelationship') && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
:current-id="currentData._id"
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_Wysiwyg
class="inputWrapper"
v-if="field.type === 'wysiwyg' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="(retrieveFieldValue(currentData, field.id)) ? retrieveFieldValue(currentData, field.id) : ''"
:isNew="currentData.isNew"
:editMode="editMode"
:current-id="currentData._id"
@signal-input="reactToFieldUpdate($event, field)"
/>
</div>
</div>
@ -172,7 +172,7 @@
</template>
<script lang="ts">
import { Component, Watch, Prop } from "vue-property-decorator"
import { Component, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
@ -181,21 +181,20 @@ import { I_OpenedDocument } from "src/interfaces/I_OpenedDocument"
import PouchDB from "pouchdb"
// import { cleanDatabases } from "src/databaseManager/cleaner"
import { single_changeRelationshipToAnotherObject, many_changeRelationshipToAnotherObject } from "src/databaseManager/relationshipManager"
import { I_KeyPressObject } from "src/interfaces/I_KeypressObject"
import { extend } from "quasar"
import Field_Break from "src/components/Field_Break.vue"
import Field_Text from "src/components/Field_Text.vue"
import Field_Number from "src/components/Field_Number.vue"
import Field_Switch from "src/components/Field_Switch.vue"
import Field_ColorPicker from "src/components/Field_ColorPicker.vue"
import Field_List from "src/components/Field_List.vue"
import Field_SingleSelect from "src/components/Field_SingleSelect.vue"
import Field_MultiSelect from "src/components/Field_MultiSelect.vue"
import Field_SingleRelationship from "src/components/Field_SingleRelationship.vue"
import Field_MultiRelationship from "src/components/Field_MultiRelationship.vue"
import Field_Wysiwyg from "src/components/Field_Wysiwyg.vue"
import Field_Break from "src/components/fields/Field_Break.vue"
import Field_Text from "src/components/fields/Field_Text.vue"
import Field_Number from "src/components/fields/Field_Number.vue"
import Field_Switch from "src/components/fields/Field_Switch.vue"
import Field_ColorPicker from "src/components/fields/Field_ColorPicker.vue"
import Field_List from "src/components/fields/Field_List.vue"
import Field_SingleSelect from "src/components/fields/Field_SingleSelect.vue"
import Field_MultiSelect from "src/components/fields/Field_MultiSelect.vue"
import Field_SingleRelationship from "src/components/fields/Field_SingleRelationship.vue"
import Field_MultiRelationship from "src/components/fields/Field_MultiRelationship.vue"
import Field_Wysiwyg from "src/components/fields/Field_Wysiwyg.vue"
import console from "console"
@Component({
@ -226,12 +225,18 @@ export default class PageDocumentDisplay extends BaseClass {
// Check if the objects exists in a database
const CurrentObjectDB = new PouchDB(this.$route.params.type)
let retrievedObject = false as unknown as I_OpenedDocument
try { retrievedObject = await CurrentObjectDB.get(this.$route.params.id) } catch (error) {}
try {
retrievedObject = await CurrentObjectDB.get(this.$route.params.id)
}
catch (error) {}
if (!retrievedObject) {
const snapshot: I_OpenedDocument[] = extend(true, [], this.SGET_allOpenedDocuments.docs)
retrievedObject = snapshot.find(s => this.$route.params.id === s._id) as unknown as I_OpenedDocument
if (retrievedObject?.isNew || retrievedObject?.editMode) { this.editMode = true }
} else {
if (retrievedObject?.isNew || retrievedObject?.editMode) {
this.editMode = true
}
}
else {
retrievedObject = (this.SGET_openedDocument(retrievedObject._id)) ? this.SGET_openedDocument(retrievedObject._id) : retrievedObject
this.editMode = (this.SGET_openedDocument(retrievedObject._id)?.hasEdits || this.SGET_openedDocument(retrievedObject._id)?.editMode)
}
@ -239,9 +244,11 @@ export default class PageDocumentDisplay extends BaseClass {
// Either create a new document or load existing one
this.currentData = (retrievedObject) ? extend(true, [], retrievedObject) : this.createNewDocumentObject()
const objectFields = this.checkObjectFields()
const objectFields = await this.checkObjectFields()
if (!objectFields) { return }
if (!objectFields) {
return
}
this.currentData.extraFields = objectFields
@ -390,13 +397,17 @@ export default class PageDocumentDisplay extends BaseClass {
const CurrentObjectDB = new PouchDB(this.$route.params.type)
let currentDocument = false as unknown as I_OpenedDocument
try { currentDocument = await CurrentObjectDB.get(this.$route.params.id) } catch (error) {}
try {
currentDocument = await CurrentObjectDB.get(this.$route.params.id)
}
catch (error) {}
let documentCopy = {} as unknown as I_OpenedDocument
if (currentDocument) {
documentCopy = extend(true, {}, this.currentData)
documentCopy._rev = currentDocument?._rev
} else {
}
else {
documentCopy = extend(true, {}, this.currentData)
}
@ -470,22 +481,25 @@ export default class PageDocumentDisplay extends BaseClass {
editMode = false
@Prop() readonly pushedKey!: I_KeyPressObject
@Watch("pushedKey", { deep: true })
processKeyPress (keypress: I_KeyPressObject) {
// Save document - CTRL + S
if (this.editMode && !keypress.shiftKey && keypress.ctrlKey && !keypress.altKey && keypress.keyCode === 83) {
this.saveDocument().catch(e => { console.log(e) })
/**
* React to keypresses
*/
@Watch("SGET_getCurrentKeyBindData", { deep: true })
processKeyPush () {
// Save document
if (this.determineKeyBind("saveDocument") && this.editMode) {
this.saveDocument().catch(e => {
console.log(e)
})
}
// Edit document - CTRL + E
if (!this.editMode && !keypress.shiftKey && keypress.ctrlKey && !keypress.altKey && keypress.keyCode === 69) {
if (this.determineKeyBind("editDocument") && !this.editMode) {
this.toggleEditMode()
}
// Delete dialog - CTRL + D
if (keypress.ctrlKey && !keypress.shiftKey && keypress.ctrlKey && !keypress.altKey && keypress.keyCode === 68) {
if (this.determineKeyBind("deleteDocument")) {
this.openDeleteDialog()
}
}
@ -500,7 +514,10 @@ export default class PageDocumentDisplay extends BaseClass {
const CurrentObjectDB = new PouchDB(this.$route.params.type)
let currentDocument = false as unknown as I_OpenedDocument
try { currentDocument = await CurrentObjectDB.get(this.$route.params.id) } catch (error) {}
try {
currentDocument = await CurrentObjectDB.get(this.$route.params.id)
}
catch (error) {}
const documentCopy: I_OpenedDocument = extend(true, {}, this.currentData)
documentCopy._rev = currentDocument?._rev
@ -528,15 +545,19 @@ export default class PageDocumentDisplay extends BaseClass {
return this.SGET_blueprint(this.$route.params.type)
}
checkObjectFields () {
async checkObjectFields () {
const currentExtraFields = (this.currentData && this.currentData.extraFields) ? this.currentData.extraFields : []
const blueprint = this.retrieveDocumentBlueprint()
if (!blueprint) { return false }
if (!blueprint) {
return false
}
blueprint.extraFields.forEach(field => {
const exists = currentExtraFields.find(f => { return f.id === field.id })
for (const field of blueprint.extraFields) {
const exists = currentExtraFields.find(f => {
return f.id === field.id
})
if (!exists) {
if (field.id === "name") {
@ -546,11 +567,50 @@ export default class PageDocumentDisplay extends BaseClass {
value: `New ${this.bluePrintData.nameSingular.toLowerCase()}`
}
)
} else {
}
else if (field.id === "parentDoc") {
if (this.$route.query?.parent) {
// Check if the objects exists in a database
const CurrentObjectDB = new PouchDB(this.$route.params.type)
const parentID = this.$route.query.parent as string
let retrievedObject = false as unknown as I_OpenedDocument
try {
retrievedObject = await CurrentObjectDB.get(parentID)
}
catch (error) {}
console.log(retrievedObject)
currentExtraFields.push(
{
id: "parentDoc",
value: {
value: {
_id: retrievedObject._id,
value: retrievedObject._id,
type: this.bluePrintData._id,
disable: false,
url: retrievedObject.url,
label: this.retrieveFieldValue(retrievedObject, "name"),
pairedField: ""
},
addedValues: {
pairedId: "",
value: ""
}
}
}
)
}
else {
currentExtraFields.push({ id: field.id, value: "" })
}
}
else {
currentExtraFields.push({ id: field.id, value: "" })
}
}
})
}
return currentExtraFields
}

View file

@ -7,6 +7,7 @@ import Vuex from "vuex"
import blueprintsModule from "./module-blueprints"
import openedDocumentsModule from "./module-openedDocuments"
import keybindsModule from "./module-keybinds"
/*
* If not building with SSR mode, you can
@ -26,7 +27,8 @@ export default store(function ({ Vue }) {
const Store = new Vuex.Store<StateInterface>({
modules: {
blueprintsModule,
openedDocumentsModule
openedDocumentsModule,
keybindsModule
// example
},

View file

@ -11,7 +11,8 @@ const mutation: MutationTree<BlueprintStateInterface> = {
const index = state.blueprints.findIndex((single: I_Blueprint) => blueprint._id === single._id)
if (index !== -1) {
state.blueprints[index] = blueprint
} else {
}
else {
state.blueprints.push(blueprint)
}
}

View file

@ -0,0 +1,11 @@
import { ActionTree } from "vuex"
import { StateInterface } from "../index"
import { KeybindsStateInterface } from "./state"
const actions: ActionTree<KeybindsStateInterface, StateInterface> = {
// someAction (context) {
// }
}
export default actions

View file

@ -0,0 +1,11 @@
import { GetterTree } from "vuex"
import { StateInterface } from "../index"
import { KeybindsStateInterface } from "./state"
const getters: GetterTree<KeybindsStateInterface, StateInterface> = {
getCurrentKeyBindData (context) {
return context.keyManagement
}
}
export default getters

View file

@ -0,0 +1,16 @@
import { Module } from "vuex"
import { StateInterface } from "../index"
import state, { KeybindsStateInterface } from "./state"
import actions from "./actions"
import getters from "./getters"
import mutations from "./mutations"
const keybindsModule: Module<KeybindsStateInterface, StateInterface> = {
namespaced: true,
actions,
getters,
mutations,
state
}
export default keybindsModule

View file

@ -0,0 +1,97 @@
import { MutationTree } from "vuex"
import { KeybindsStateInterface } from "./state"
import { I_KeyPressObject } from "./../../interfaces/I_KeypressObject"
import { uid } from "quasar"
const resetCurrentKey = () => {
return {
altKey: true,
ctrlKey: true,
shiftKey: true,
id: "",
keyCode: 99999
}
}
const mutation: MutationTree<KeybindsStateInterface> = {
registerDefaultKeybind (state: KeybindsStateInterface, input: I_KeyPressObject) {
if (!input.id) {
return
}
state.keyManagement.currentKeyPress = resetCurrentKey()
const existingIndex = state.keyManagement.defaults.findIndex(e => e.id === input.id)
// Ovewrite existing if it exists
if (existingIndex > -1) {
state.keyManagement.defaults[existingIndex] = input
}
// Otherwise, add a new one altogether
else {
state.keyManagement.defaults.push(input)
}
state.keyManagement.timestamp = uid()
},
deregisterDefaultKeybind (state: KeybindsStateInterface, input: I_KeyPressObject) {
if (!input.id) {
return
}
state.keyManagement.currentKeyPress = resetCurrentKey()
const existingIndex = state.keyManagement.defaults.findIndex(e => e.id === input.id)
// Remove the existing keybind
if (existingIndex > -1) {
state.keyManagement.defaults.splice(existingIndex, 1)
state.keyManagement.timestamp = uid()
}
},
registerUserKeybind (state: KeybindsStateInterface, input: I_KeyPressObject) {
if (!input.id) {
return
}
state.keyManagement.currentKeyPress = resetCurrentKey()
const existingIndex = state.keyManagement.userKeybinds.findIndex(e => e.id === input.id)
// Ovewrite existing if it exists
if (existingIndex > -1) {
state.keyManagement.userKeybinds[existingIndex] = input
}
// Otherwise, add a new one altogether
else {
state.keyManagement.userKeybinds.push(input)
}
state.keyManagement.timestamp = uid()
},
deregisterUserKeybind (state: KeybindsStateInterface, input: I_KeyPressObject) {
if (!input.id) {
return
}
state.keyManagement.currentKeyPress = resetCurrentKey()
const existingIndex = state.keyManagement.userKeybinds.findIndex(e => e.id === input.id)
// Remove the existing keybind
if (existingIndex > -1) {
state.keyManagement.userKeybinds.splice(existingIndex, 1)
state.keyManagement.timestamp = uid()
}
},
updatePressedKey (state: KeybindsStateInterface, input: I_KeyPressObject) {
state.keyManagement.currentKeyPress = input
state.keyManagement.timestamp = uid()
}
}
export default mutation

View file

@ -0,0 +1,31 @@
import { I_KeyPressObject } from "./../../interfaces/I_KeypressObject"
export interface KeybindsStateInterface {
keyManagement: KeyManagementInterface
}
export interface KeyManagementInterface {
timestamp: string,
userKeybinds: I_KeyPressObject[]
defaults: I_KeyPressObject[]
currentKeyPress: I_KeyPressObject
}
function state (): KeybindsStateInterface {
return {
keyManagement: {
timestamp: "",
userKeybinds: [],
defaults: [],
currentKeyPress: {
altKey: true,
ctrlKey: true,
shiftKey: true,
id: "",
keyCode: 99999
}
}
}
}
export default state

View file

@ -3,9 +3,9 @@
- Add on-the-fly generation of non-existent 2-way relationships
- Add subtabs (character stats, general info, etc)
- Add colored backgrounds to documents (along with already existingt colored texts)
- Custom icons/images to documents
- Middle mouse button close tab
- Add option to make the command buttons for document sticky (save/delete/edit)
- Allow/Disallow default document types
- Dark mode