fantasia-archive/src/components/ObjectTree.vue

694 lines
19 KiB
Vue
Raw Normal View History

2021-01-31 02:43:13 +13:00
<template>
<span>
2021-03-04 13:27:07 +13:00
<q-input ref="treeFilter" dark filled v-model="treeFilter" label="Filter document tree...">
2021-01-31 02:43:13 +13:00
<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
2021-02-09 15:21:48 +13:00
class="objectTree q-pa-sm"
:nodes="hierarchicalTree"
node-key="key"
2021-01-31 02:43:13 +13:00
no-connectors
ref="tree"
dark
:filter="treeFilter"
:selected.sync="selectedTreeNode"
2021-02-26 14:50:46 +13:00
:expanded.sync="expandedTreeNodes"
2021-01-31 02:43:13 +13:00
>
<template v-slot:default-header="prop">
<div class="row items-center col-grow" @click.stop.prevent="processNodeClick(prop.node)">
<div class="documentLabel"
:style="`color: ${prop.node.color}`"
@click.stop.prevent.middle="processNodeLabelMiddleClick(prop.node)"
>
2021-02-26 14:50:46 +13:00
<q-icon
:style="`color: ${determineNodeColor(prop.node)}; width: 22px !important;`"
2021-02-26 14:50:46 +13:00
:size="(prop.node.icon.includes('fas')? '16px': '21px')"
:name="prop.node.icon"
class="q-mr-sm self-center" />
{{ prop.node.label }}
<span
class="text-primary text-weight-medium q-ml-xs"
v-if="prop.node.isRoot">
({{prop.node.documentCount}})
</span>
2021-01-31 02:43:13 +13:00
<q-badge
2021-02-26 14:50:46 +13:00
class="treeBadge"
:class="{'noChilden': prop.node.children.length === 0}"
2021-02-26 14:50:46 +13:00
v-if="prop.node.sticker"
color="primary"
outline
floating
>
{{prop.node.sticker}}
<q-tooltip
:delay="500"
>
2021-01-31 02:43:13 +13:00
Order priority of the document
</q-tooltip>
</q-badge>
<div class="treeButtonGroup">
<q-btn
tabindex="-1"
v-if="prop.node.children && prop.node.children.length > 0 && !prop.node.isRoot && !prop.node.isTag"
round
2021-02-26 14:50:46 +13:00
flat
dense
2021-02-26 14:50:46 +13:00
color="dark"
class="z-1 q-ml-sm treeButton treeButton--edit"
icon="mdi-pencil"
size="8px"
2021-02-26 14:50:46 +13:00
@click.stop.prevent="openExistingDocumentRoute(prop.node)"
>
<q-tooltip
:delay="300"
>
2021-02-26 14:50:46 +13:00
Open/Edit {{ prop.node.label }}
</q-tooltip>
</q-btn>
<q-btn
tabindex="-1"
v-if="(!prop.node.specialLabel && !prop.node.isRoot) || (prop.node.isRoot && !prop.node.isTag)"
round
2021-02-26 14:50:46 +13:00
flat
dense
2021-02-26 14:50:46 +13:00
color="dark"
class="z-1 q-ml-sm treeButton treeButton--add"
icon="mdi-plus"
size="8px"
2021-02-26 14:50:46 +13:00
@click.stop.prevent="processNodeNewDocumentButton(prop.node)"
>
<q-tooltip
:delay="300"
>
Add a new document belonging under {{ prop.node.label }}
</q-tooltip>
</q-btn>
</div>
</div>
2021-01-31 02:43:13 +13:00
</div>
2021-01-31 02:43:13 +13:00
</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"
2021-01-31 02:43:13 +13:00
import BaseClass from "src/BaseClass"
2021-02-28 06:00:57 +13:00
import { I_OpenedDocument, I_ShortenedDocument } from "src/interfaces/I_OpenedDocument"
2021-01-31 02:43:13 +13:00
import { I_NewObjectTrigger } from "src/interfaces/I_NewObjectTrigger"
import PouchDB from "pouchdb"
2021-02-26 14:50:46 +13:00
import { engageBlueprints, retrieveAllBlueprints } from "src/scripts/databaseManager/blueprintManager"
// import { cleanDatabases } from "src/scripts/databaseManager/cleaner"
2021-01-31 02:43:13 +13:00
import { I_Blueprint } from "src/interfaces/I_Blueprint"
import { extend, colors } from "quasar"
import { tagListBuildFromBlueprints } from "src/scripts/utilities/tagListBuilder"
2021-01-31 02:43:13 +13:00
@Component({
components: { }
})
export default class ObjectTree extends BaseClass {
/****************************************************************/
// KEYBINDS MANAGEMENT
/****************************************************************/
@Watch("SGET_getCurrentKeyBindData", { deep: true })
processKeyPush () {
// Focus left tree search
if (this.determineKeyBind("focusHierarchicalTree")) {
2021-02-21 01:06:21 +13:00
const treeFilterDOM = this.$refs.treeFilter as unknown as HTMLInputElement
treeFilterDOM.focus()
}
// Clear input in the left tree search
if (this.determineKeyBind("clearInputHierarchicalTree")) {
2021-02-21 01:06:21 +13:00
this.resetTreeFilter()
}
}
/****************************************************************/
// GENERIC FUNCTIONALITY
/****************************************************************/
2021-01-31 02:43:13 +13:00
/**
* Load all blueprints and build the tree out of them
2021-01-31 02:43:13 +13:00
*/
async created () {
// await cleanDatabases()
await this.processBluePrints()
2021-01-31 02:43:13 +13:00
// Unfuck the rendering by giving the app some time to load first
2021-03-04 13:27:07 +13:00
await this.$nextTick()
this.buildCurrentObjectTree().catch((e) => {
console.log(e)
})
}
/****************************************************************/
// BLUEPRINT MANAGEMENT
/****************************************************************/
2021-01-31 02:43:13 +13:00
/**
* In case any of the blueprints change, reload the whole tree
*/
2021-01-31 02:43:13 +13:00
@Watch("SGET_allBlueprints", { deep: true })
reactToBluePrintRefresh () {
this.buildCurrentObjectTree().catch((e) => {
console.log(e)
})
2021-01-31 02:43:13 +13:00
}
/**
* 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)
2021-01-31 02:43:13 +13:00
}
/****************************************************************/
// HIERARCHICAL TREE - HELPERS AND MODELS
/****************************************************************/
2021-01-31 02:43:13 +13:00
/**
* 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
}
}
/**
*
*/
2021-01-31 02:43:13 +13:00
@Watch("SGET_allOpenedDocuments", { deep: true })
2021-02-28 06:00:57 +13:00
async reactToDocumentListChange (val: { treeAction: boolean, docs: I_OpenedDocument[]}) {
if (val.treeAction) {
await this.buildCurrentObjectTree()
this.buildTreeExpands(val?.docs)
this.lastDocsSnapShot = extend(true, [], val.docs)
}
else if (val.docs.length !== this.lastDocsSnapShot.length) {
this.lastDocsSnapShot = extend(true, [], val.docs)
}
2021-01-31 02:43:13 +13:00
}
2021-02-28 06:00:57 +13:00
lastDocsSnapShot:I_OpenedDocument[] = []
/**
* 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}[] = []
/**
* A resetter for the currently selected node
*/
selectedTreeNode = null
2021-02-26 14:50:46 +13:00
/**
* Holds all currently expanded notes
*/
expandedTreeNodes = []
/**
* 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
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
treeFilterDOM.focus()
}
/****************************************************************/
// HIERARCHICAL TREE - CONTENT CONSTRUCTION
/****************************************************************/
/**
* Sort the whole tree via alphabetical and custom numeric order
* @param input Hierartchical tree object to sort
*/
2021-01-31 02:43:13 +13:00
sortDocuments (input: I_ShortenedDocument[]) {
input
// Sort by name
2021-01-31 02:43:13 +13:00
.sort((a, b) => a.label.localeCompare(b.label))
// Sort by custom order
2021-01-31 02:43:13 +13:00
.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
}
2021-01-31 02:43:13 +13:00
return 0
})
2021-02-28 06:00:57 +13:00
// Put the number value on top of the list and alphabetical below them
input = [
...input.filter(e => e.extraFields.find(e => e.id === "order")?.value),
...input.filter(e => !e.extraFields.find(e => e.id === "order")?.value)
]
2021-01-31 02:43:13 +13:00
input.forEach((e, i) => {
// Run recursive if the node has any children
if (e.children.length > 0) {
input[i].children = this.sortDocuments(input[i].children)
}
2021-01-31 02:43:13 +13:00
})
return input
}
/**
* Builds proper hiearachy for flat array of documents
* @param input Non-hierarchical tree to build the hiearachy out of
*/
2021-01-31 02:43:13 +13:00
buildTreeHierarchy (input: I_ShortenedDocument[]) {
const map: number[] = []
let node
const roots = []
let i
for (i = 0; i < input.length; i += 1) {
// Initialize the map
map[input[i]._id] = i
2021-01-31 02:43:13 +13:00
}
for (i = 0; i < input.length; i += 1) {
node = input[i]
if (node.parentDoc !== false) {
// If there are any dangling branches check that map[node.parentDoc] exists
2021-01-31 02:43:13 +13:00
if (input[map[node.parentDoc]]) {
input[map[node.parentDoc]].children.push(node)
}
else {
2021-01-31 02:43:13 +13:00
roots.push(node)
}
}
else {
2021-01-31 02:43:13 +13:00
roots.push(node)
}
}
const sortedRoots = this.sortDocuments(roots)
return sortedRoots
}
/**
* Builds a brand new sparkling hearchy tree out of available data
*/
2021-01-31 02:43:13 +13:00
async buildCurrentObjectTree () {
const allBlueprings = this.SGET_allBlueprints
const treeObject: any[] = []
let allTreeDocuments: I_ShortenedDocument[] = []
2021-01-31 02:43:13 +13:00
// Process all documents, build hieararchy out of the and sort them via name and custom order
2021-01-31 02:43:13 +13:00
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
2021-02-09 15:21:48 +13:00
const parentDocID = doc.extraFields.find(e => e.id === "parentDoc")?.value.value as unknown as {_id: string}
2021-02-21 01:06:21 +13:00
const color = doc.extraFields.find(e => e.id === "documentColor")?.value as unknown as string
const isCategory = doc.extraFields.find(e => e.id === "categorySwitch")?.value as unknown as string
2021-01-31 02:43:13 +13:00
return {
label: doc.extraFields.find(e => e.id === "name")?.value,
2021-02-21 01:06:21 +13:00
icon: (isCategory) ? "fas fa-folder-open" : doc.icon,
2021-01-31 02:43:13 +13:00
sticker: doc.extraFields.find(e => e.id === "order")?.value,
parentDoc: (parentDocID) ? parentDocID._id : false,
handler: this.openExistingDocumentRoute,
2021-01-31 02:43:13 +13:00
expandable: true,
2021-02-21 01:06:21 +13:00
color: color,
2021-01-31 02:43:13 +13:00
type: doc.type,
children: [],
hasEdits: false,
isNew: false,
url: doc.url,
extraFields: (doc?.extraFields) || [],
_id: singleDocument.id,
key: singleDocument.id
2021-01-31 02:43:13 +13:00
} as I_ShortenedDocument
})
const documentCount = allDocumentsRows.length
const listCopy: I_ShortenedDocument[] = extend(true, [], allDocumentsRows)
allTreeDocuments = [...allTreeDocuments, ...listCopy]
2021-01-31 02:43:13 +13:00
const hierarchicalTreeContent = this.buildTreeHierarchy(allDocumentsRows)
const treeRow = {
label: blueprint.namePlural,
icon: blueprint.icon,
order: blueprint.order,
2021-01-31 02:43:13 +13:00
_id: blueprint._id,
key: blueprint._id,
handler: this.addNewObjectRoute,
2021-01-31 02:43:13 +13:00
specialLabel: blueprint.nameSingular.toLowerCase(),
isRoot: true,
documentCount: documentCount,
2021-01-31 02:43:13 +13:00
children: [
...hierarchicalTreeContent,
{
label: `Add new ${blueprint.nameSingular.toLowerCase()}`,
icon: "mdi-plus",
handler: this.addNewObjectRoute,
children: false,
key: `${blueprint._id}_add`,
_id: blueprint._id,
specialLabel: blueprint.nameSingular.toLowerCase()
2021-01-31 02:43:13 +13:00
}
]
}
treeObject.push(treeRow)
}
// Sort the top level of the blueprints
treeObject.sort((a, b) => {
if (a.order < b.order) {
return 1
}
if (a.order > b.order) {
return -1
}
return 0
})
const tagList = await tagListBuildFromBlueprints(this.SGET_allBlueprints)
tagList.forEach((tag: string) => {
const tagDocs = allTreeDocuments
.filter(doc => {
const docTags = doc.extraFields.find(e => e.id === "tags")?.value as unknown as string[]
return (docTags && docTags.includes(tag))
})
.map((doc:I_ShortenedDocument) => {
// @ts-ignore
doc.key = `${tag}${doc._id}`
// @ts-ignore
doc.isTag = true
return doc
})
.sort((a, b) => a.label.localeCompare(b.label))
const tagObject = {
label: `${tag}`,
icon: "mdi-tag",
_id: `tag-${tag}`,
key: `tag-${tag}`,
documentCount: tagDocs.length,
isRoot: true,
isTag: true,
children: tagDocs
}
treeObject.push(tagObject)
})
// Assign the finished object to the render model
this.hierarchicalTree = treeObject
2021-01-31 02:43:13 +13:00
}
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)
}
}
2021-01-31 02:43:13 +13:00
2021-02-28 06:00:57 +13:00
buildTreeExpands (newDocs: I_OpenedDocument[]) {
const expandIDs: string[] = []
// Check for parent changes
newDocs.forEach(s => {
const oldParentDoc = this.lastDocsSnapShot.find(doc => doc._id === s._id)
// Fizzle if the parent doesn't exist in the old version
if (!oldParentDoc) {
return false
}
const oldParentDocField = this.retrieveFieldValue(oldParentDoc, "parentDoc")
// @ts-ignore
const oldParentDocID = (oldParentDocField?.value) ? oldParentDocField.value.value : ""
const newParentDocField = this.retrieveFieldValue(s, "parentDoc")
// @ts-ignore
const newParentDocID = (newParentDocField?.value) ? newParentDocField.value.value : ""
if ((newParentDocID !== oldParentDocID) || (newParentDocID && oldParentDoc.isNew)) {
expandIDs.push(newParentDocID)
}
})
// Process top level documents
newDocs.forEach(s => {
const newParentDocField = this.retrieveFieldValue(s, "parentDoc")
// @ts-ignore
const newParentDocID = (newParentDocField?.value) ? newParentDocField.value.value : false
if (!newParentDocID) {
expandIDs.push(s.type)
}
})
expandIDs.forEach(s => {
2021-02-28 06:00:57 +13:00
this.recursivelyExpandNode(s)
})
}
recursivelyExpandNode (nodeID: string) {
const treeDOM = this.$refs.tree as unknown as {
setExpanded: (key:string, state: boolean)=> void
getNodeByKey: (key:string)=> void
}
// @ts-ignore
this.expandedTreeNodes = [...new Set([
...this.expandedTreeNodes,
nodeID
])]
const currentTreeNode = (treeDOM.getNodeByKey(nodeID)) as unknown as {parentDoc: string, type: string}
// Dig into the upper hierarchy
if (currentTreeNode?.parentDoc) {
this.recursivelyExpandNode(currentTreeNode.parentDoc)
}
// If we are at the top of the tree, expand the top category
else if (currentTreeNode?.type) {
// @ts-ignore
this.expandedTreeNodes = [...new Set([
...this.expandedTreeNodes,
currentTreeNode.type
])]
}
}
processNodeLabelMiddleClick (node: {
key: string
_id: string
children: []
type: string
isRoot: boolean
isTag: boolean
specialLabel: string|boolean
}) {
this.selectedTreeNode = null
if (node.isRoot && node.isTag) {
return
}
if (!node.specialLabel && !node.isRoot) {
// @ts-ignore
this.openExistingDocumentRoute(node)
}
else {
this.addNewObjectRoute(node)
}
2021-01-31 02:43:13 +13:00
}
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)
}
}
2021-01-31 02:43:13 +13:00
expandeCollapseNode (node: {key: string}) {
const treeDOM = this.$refs.tree as unknown as {
setExpanded: (key:string, state: boolean)=> void,
isExpanded: (key:string)=> boolean
}
2021-01-31 02:43:13 +13:00
const isExpanded = treeDOM.isExpanded(node.key)
treeDOM.setExpanded(node.key, !isExpanded)
2021-01-31 02:43:13 +13:00
}
determineNodeColor (node: {color: string, isTag: boolean, isRoot: boolean}) {
// @ts-ignore
return (node?.isTag && node?.isRoot) ? colors.getBrand("primary") : node.color
}
2021-01-31 02:43:13 +13:00
}
</script>
2021-02-09 15:21:48 +13:00
<style lang="scss">
.objectTree {
.q-tree__arrow {
margin-right: 0;
2021-02-26 14:50:46 +13:00
padding: 4px 4px 4px 0;
}
.q-tree__node-header {
padding: 0;
2021-02-09 15:21:48 +13:00
}
.documentLabel {
width: 100%;
display: flex;
justify-content: space-between;
2021-02-26 14:50:46 +13:00
padding: 4px 4px 4px 4px;
}
.treeButtonGroup {
flex-grow: 0;
flex-shrink: 0;
display: flex;
height: fit-content;
margin-left: auto;
align-self: center;
2021-02-09 15:21:48 +13:00
}
}
2021-02-26 14:50:46 +13:00
.treeBadge {
left: inherit;
right: calc(100% + 3px);
padding: 3px 2px;
border: none;
background: rgba($primary, 0.15);
top: 50%;
transform: translateY(-50%);
min-width: 24px;
justify-content: center;
&.noChilden {
right: calc(100% + 23px);
}
2021-02-26 14:50:46 +13:00
}
2021-02-26 14:50:46 +13:00
.treeButton {
&--add {
.q-icon {
font-size: 20px;
2021-02-26 14:50:46 +13:00
color: $primary;
}
}
&--edit {
.q-icon {
font-size: 14px;
2021-02-26 14:50:46 +13:00
color: #fff;
}
}
}
2021-02-09 15:21:48 +13:00
</style>