added tag support and further improved search

This commit is contained in:
Elvanos 2021-03-03 02:10:05 +01:00
parent 7d2597e3c8
commit d9a68cf0cf
30 changed files with 457 additions and 24 deletions

View file

@ -10,16 +10,21 @@
- A new logo added to the app (better visibility of the logo in small scales and icons)
- Massive overhaul of the search engine used by the Quick opening existing document and single/multi relationship fields (now supports tags, categories, document types, inteligent filtering and inteligent sorting via importance of the found values)
- Added color support to single/multi relationship fields
- Added a hierarchical path to Quick opening existing document and single/multi relationship fields
- Added filtering to include or exclude documents that are considered categories in the Quick opening existing document dialog
- Added automatic opening of hierarchical tree branches upon adding/moving documents under/among them
- Added tags support
### QoL adjustments
- Added a hierarchical path to Quick opening existing document and single/multi relationship fields
- Added color support to single/multi relationship fields
- Added filtering to include or exclude documents that are considered categories in the Quick opening existing document dialog
- Slightly modified the scrollbar visuals to be less intrusive
- Added a light golden tint to the background of the app to go easy on user's eyes before farkmode is added
- Improved performance by reducing the amount of time the side-tree re-renders
- Added automatic opening of hierarchical tree branches upon adding/moving documents under/among them
- Alligned custom order sorting for both nodes with and without children
- Visually alligned custom order badge for both nodes with and without children
- Added dark visuals to the single-select and multi-select fields to align with thge rest of the app
- All popup dialogs have been unified to dark-color mode
- Prettified a dialog popup for confirmation of closing a document with active edits
- Added a small filter over the big white areas to ease-up on the user's eyes before darkmode is added
## 0.1.2

View file

@ -1,6 +1,6 @@
{
"name": "fantasiaarchive",
"version": "0.1.2",
"version": "0.1.3",
"description": "A database manager for world building",
"productName": "Fantasia archive",
"author": "Elvanos <elvanos66@gmail.com>",

View file

@ -44,6 +44,7 @@ export default class AppHeader extends BaseClass {
}
.appHeaderInner {
z-index: 999999;
display: flex;
min-height: 40px;
-webkit-app-region: drag;

View file

@ -28,7 +28,7 @@
@click.stop.prevent.middle="processNodeLabelMiddleClick(prop.node)"
>
<q-icon
:style="`color: ${prop.node.color}; width: 22px !important;`"
:style="`color: ${determineNodeColor(prop.node)}; width: 22px !important;`"
:size="(prop.node.icon.includes('fas')? '16px': '21px')"
:name="prop.node.icon"
class="q-mr-sm self-center" />
@ -56,7 +56,7 @@
<div class="treeButtonGroup">
<q-btn
tabindex="-1"
v-if="prop.node.children && prop.node.children.length > 0 && !prop.node.isRoot"
v-if="prop.node.children && prop.node.children.length > 0 && !prop.node.isRoot && !prop.node.isTag"
round
flat
dense
@ -74,7 +74,7 @@
</q-btn>
<q-btn
tabindex="-1"
v-if="!prop.node.specialLabel || prop.node.isRoot"
v-if="(!prop.node.specialLabel && !prop.node.isRoot) || (prop.node.isRoot && !prop.node.isTag)"
round
flat
dense
@ -134,7 +134,8 @@ import PouchDB from "pouchdb"
import { engageBlueprints, retrieveAllBlueprints } from "src/scripts/databaseManager/blueprintManager"
// import { cleanDatabases } from "src/scripts/databaseManager/cleaner"
import { I_Blueprint } from "src/interfaces/I_Blueprint"
import { extend } from "quasar"
import { extend, colors } from "quasar"
import { tagListBuildFromBlueprints } from "src/scripts/utilities/tagListBuilder"
@Component({
components: { }
@ -268,6 +269,8 @@ export default class ObjectTree extends BaseClass {
resetTreeFilter () {
this.treeFilter = ""
const treeFilterDOM = this.$refs.treeFilter as unknown as HTMLInputElement
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
treeFilterDOM.focus()
}
@ -358,6 +361,7 @@ export default class ObjectTree extends BaseClass {
async buildCurrentObjectTree () {
const allBlueprings = this.SGET_allBlueprints
const treeObject: any[] = []
let allTreeDocuments: I_ShortenedDocument[] = []
// Process all documents, build hieararchy out of the and sort them via name and custom order
for (const blueprint of allBlueprings) {
@ -393,6 +397,8 @@ export default class ObjectTree extends BaseClass {
})
const documentCount = allDocumentsRows.length
const listCopy: I_ShortenedDocument[] = extend(true, [], allDocumentsRows)
allTreeDocuments = [...allTreeDocuments, ...listCopy]
const hierarchicalTreeContent = this.buildTreeHierarchy(allDocumentsRows)
@ -436,6 +442,36 @@ export default class ObjectTree extends BaseClass {
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
}
@ -539,10 +575,15 @@ export default class ObjectTree extends BaseClass {
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)
@ -582,6 +623,11 @@ export default class ObjectTree extends BaseClass {
const isExpanded = treeDOM.isExpanded(node.key)
treeDOM.setExpanded(node.key, !isExpanded)
}
determineNodeColor (node: {color: string, isTag: boolean, isRoot: boolean}) {
// @ts-ignore
return (node?.isTag && node?.isRoot) ? colors.getBrand("primary") : node.color
}
}
</script>

View file

@ -47,6 +47,17 @@
<q-item-section>
<q-item-label v-html="opt.label" ></q-item-label>
<q-item-label caption class="text-cultured" v-html="opt.hierarchicalPath"></q-item-label>
<q-item-label caption class="text-cultured" v-if="opt.tags">
<q-chip
v-for="(input,index) in opt.tags" :key="index"
outline
style="opacity: 0.8;"
size="12px"
class="bg-dark text-cultured"
v-html="`${input}`"
>
</q-chip>
</q-item-label>
</q-item-section>
<q-btn
tabindex="-1"
@ -127,6 +138,7 @@ export default class ExistingDocumentDialog extends DialogBase {
type: doc.type,
// @ts-ignore
hierarchicalPath: this.getDocumentHieararchicalPath(doc, dbDocuments.rows),
tags: doc.extraFields.find(e => e.id === "tags")?.value,
color: doc.extraFields.find(e => e.id === "documentColor")?.value,
isCategory: doc.extraFields.find(e => e.id === "categorySwitch")?.value
} as unknown as I_ShortenedDocument

View file

@ -18,6 +18,8 @@
ref="ref_newDocument"
style="flex-grow: 1;"
dense
menu-anchor="bottom middle"
menu-self="top middle"
class="newDocumentSelect"
:options="filteredNewInput"
use-input
@ -101,7 +103,7 @@ export default class NewDocumentDialog extends DialogBase {
setTimeout( () =>{
// @ts-ignore
this.$refs.ref_newDocument.focus()
}, 100)
}, 300)
/* eslint-enable */
})
}

View file

@ -27,6 +27,10 @@
v-if="editMode"
style="width: 100%;"
dense
dark
menu-anchor="bottom middle"
menu-self="top middle"
class="multiSelect"
:options="extraInput"
use-input
outlined

View file

@ -24,6 +24,10 @@
v-if="editMode"
style="width: 100%;"
dense
dark
menu-anchor="bottom middle"
menu-self="top middle"
class="singleSelect"
:options="extraInput"
use-input
outlined

View file

@ -0,0 +1,127 @@
<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="inputIcon.includes('fas')? '15px': '20px'" class="q-mr-md"/>
{{inputDataBluePrint.name}}
<q-icon v-if="toolTip" name="mdi-help-circle" size="16px" class="q-ml-md">
<q-tooltip :delay="500">
<span v-html="toolTip"/>
</q-tooltip>
</q-icon>
</div>
<div
v-if="!editMode"
>
<q-chip
v-for="(input,index) in localInput" :key="index"
color="primary" text-color="white" class="text-bold">
{{input}}
</q-chip>
</div>
<q-select
v-if="editMode"
style="width: 100%;"
dense
dark
menu-anchor="bottom middle"
menu-self="top middle"
class="tagSelect"
:options="allTags"
use-input
outlined
use-chips
@filter="filterFn"
input-debounce="0"
new-value-mode="add"
multiple
v-model="localInput"
@input="signalInput"
@keydown="signalInput"
>
</q-select>
<div class="separatorWrapper">
<q-separator color="grey q-mt-lg" />
</div>
</div>
</template>
<script lang="ts">
import { Component, Emit, Prop, Watch } from "vue-property-decorator"
import BaseClass from "src/BaseClass"
import { tagListBuildFromBlueprints } from "src/scripts/utilities/tagListBuilder"
import { I_ExtraFields } from "src/interfaces/I_Blueprint"
@Component({
components: { }
})
export default class Field_Tags extends BaseClass {
@Prop({ default: [] }) readonly inputDataBluePrint!: I_ExtraFields
@Prop({
default: () => {
return []
}
}) 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 : []
}
@Watch("inputDataBluePrint", { deep: true, immediate: true })
reactToBlueprintChanges () {
this.buildTagList().catch(e => console.log(e))
}
get inputIcon () {
return this.inputDataBluePrint?.icon
}
get toolTip () {
return this.inputDataBluePrint?.tooltip
}
allTags: string[] = []
filterFn (val: string, update: (fn: any) => void) {
if (val === "") {
update(() => {
if (this.inputDataBluePrint?.predefinedSelectValues) {
this.allTags = this.inputDataBluePrint.predefinedSelectValues
}
})
return
}
update(() => {
if (this.inputDataBluePrint?.predefinedSelectValues) {
const needle = val.toLowerCase()
this.allTags = this.inputDataBluePrint.predefinedSelectValues.filter(v => v.toLowerCase().indexOf(needle) > -1)
}
})
}
async buildTagList () {
this.allTags = await tagListBuildFromBlueprints(this.SGET_allBlueprints)
}
@Emit()
signalInput () {
this.changedInput = true
return this.localInput
}
}
</script>

View file

@ -66,6 +66,9 @@ body {
left: calc(100% - 30px);
}
.tagSelect,
.singleSelect,
.multiSelect,
.singleRelashionshipSelect,
.multiRelashionshipSelect,
.existingDocumentSelect,
@ -89,7 +92,7 @@ body {
}
.q-position-engine {
overflow-y: scroll !important;
overflow-y: auto !important;
}
.text-underline {

View file

@ -19,7 +19,8 @@ export interface I_ExtraFields {
"singleToManyRelationship" |
"manyToSingleRelationship" |
"manyToManyRelationship" |
"break"
"break" |
"tags"
predefinedListExtras?: {
affix?: string

View file

@ -29,6 +29,7 @@ export interface I_ShortenedDocument{
children: I_ShortenedDocument[]
extraFields: I_ExtraDocumentFields[]
color?: string
tags?: string[]
activeTypeSearch?: boolean
filteredOut?: boolean

View file

@ -171,6 +171,16 @@
@signal-input="reactToFieldUpdate($event, field)"
/>
<Field_Tags
class="inputWrapper"
v-if="field.type === 'tags' && fieldLimiter(field.id)"
:inputDataBluePrint="field"
:inputDataValue="retrieveFieldValue(currentData, field.id)"
:isNew="currentData.isNew"
:editMode="editMode"
@signal-input="reactToFieldUpdate($event, field)"
/>
</div>
</div>
@ -202,7 +212,7 @@ 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"
import Field_Tags from "src/components/fields/Field_Tags.vue"
@Component({
components: {
@ -216,7 +226,8 @@ import console from "console"
Field_MultiSelect,
Field_SingleRelationship,
Field_MultiRelationship,
Field_Wysiwyg
Field_Wysiwyg,
Field_Tags
}
})
@ -397,6 +408,18 @@ export default class PageDocumentDisplay extends BaseClass {
const dataPass = { doc: dataCopy, treeAction: false }
this.SSET_updateOpenedDocument(dataPass)
}
// FIELD - Tags
if (field.type === "tags") {
this.currentData.hasEdits = true
const indexToUpdate = this.currentData.extraFields.findIndex(s => s.id === field.id)
this.currentData.extraFields[indexToUpdate].value = inputData
const dataCopy: I_OpenedDocument = extend(true, {}, this.currentData)
const dataPass = { doc: dataCopy, treeAction: false }
this.SSET_updateOpenedDocument(dataPass)
}
}
toggleEditMode () {

View file

@ -62,6 +62,16 @@ export const chaptersBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "content",
name: "Chapter content",

View file

@ -68,6 +68,16 @@ export const charactersBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -68,6 +68,16 @@ export const currenciesBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -68,6 +68,16 @@ export const eventsBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -68,6 +68,16 @@ export const itemsBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -68,6 +68,16 @@ export const languagesBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -68,6 +68,16 @@ export const locationsBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -62,6 +62,16 @@ export const loreNotesBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "notes",
name: "Note list",

View file

@ -68,6 +68,16 @@ export const magicBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -68,6 +68,16 @@ export const mythsBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -68,6 +68,16 @@ export const politicalGroupsBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -68,6 +68,16 @@ export const racesBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -68,6 +68,16 @@ export const religionsBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -68,6 +68,16 @@ export const techBlueprint: I_Blueprint = {
`,
sizing: 2
},
{
id: "tags",
name: "Tags",
type: "tags",
icon: "mdi-tag",
tooltip:
`tags
`,
sizing: 12
},
{
id: "otherNames",
name: "Other names & Epithets",

View file

@ -40,20 +40,20 @@ export const advancedDocumentFilter = (inputString: string, documentList: I_Shor
const searchWordList = inputString.toLowerCase().split(" ")
let categorySeach = false as unknown as string
let tagSeach = false as unknown as string
let tagSearch = false as unknown as string
let typeSeach = false as unknown as string
const categorySeachIndex = searchWordList.findIndex(w => w.charAt(0) === ">")
const tagSeachIndex = searchWordList.findIndex(w => w.charAt(0) === "#")
const tagSearchIndex = searchWordList.findIndex(w => w.charAt(0) === "#")
const typeSeachIndex = searchWordList.findIndex(w => w.charAt(0) === "$")
if (categorySeachIndex >= 0) {
categorySeach = searchWordList[categorySeachIndex].substring(1)
searchWordList[categorySeachIndex] = ""
}
if (tagSeachIndex >= 0) {
tagSeach = searchWordList[tagSeachIndex].substring(1)
searchWordList[tagSeachIndex] = ""
if (tagSearchIndex >= 0) {
tagSearch = searchWordList[tagSearchIndex].substring(1)
searchWordList[tagSearchIndex] = ""
}
if (typeSeachIndex >= 0) {
typeSeach = searchWordList[typeSeachIndex].substring(1)
@ -67,7 +67,7 @@ export const advancedDocumentFilter = (inputString: string, documentList: I_Shor
/****************************************************************/
if (categorySeach) {
documentList = documentList.filter((doc, index) => {
documentList = documentList.filter(doc => {
let stringPath = doc.hierarchicalPath as unknown as string
stringPath = stringPath.toLowerCase()
stringPath = stringPath.replace(/>/gi, "")
@ -75,12 +75,36 @@ export const advancedDocumentFilter = (inputString: string, documentList: I_Shor
stringPath = stringPath.replace(/ /gi, "-")
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
doc.hierarchicalPath = `<span class="text-primary">${doc.hierarchicalPath}</span>`
doc.hierarchicalPath = `<span class="text-primary text-bold">${doc.hierarchicalPath}</span>`
return (stringPath.includes(categorySeach))
})
}
/****************************************************************/
// Tag filter
/****************************************************************/
if (tagSearch) {
documentList = documentList.filter(doc => {
let matchFound = false
if (doc.tags) {
doc.tags.forEach((tag, index) => {
const ogTag = tag
tag = tag.toLowerCase()
tag = tag.replace(/>/gi, "")
tag = tag.replace(/ {2}/gi, "-")
tag = tag.replace(/ /gi, "-")
if (tag.includes(tagSearch)) {
// @ts-ignore
doc.tags[index] = `<span class="text-primary text-bold">${ogTag}</span>`
}
matchFound = (tag.includes(tagSearch) || matchFound)
})
}
return matchFound
})
}
/****************************************************************/
// Type filter
/****************************************************************/
@ -148,13 +172,21 @@ export const advancedDocumentFilter = (inputString: string, documentList: I_Shor
// Color the word if they were found
documentWordListColoring = documentWordListColoring.map(docWord => {
if (foundWordList.includes(docWord.toLowerCase())) {
return `<span class="text-primary text-underline">${docWord}</span>`
return `<span class="text-primary text-bold">${docWord}</span>`
}
return docWord
})
doc.label = documentWordListColoring.join(" ")
})
// Cover case of exact match
documentList.map(doc => {
if (doc.exactMatch) {
doc.label = `<span class="text-primary text-bold">${doc.label}</span>`
}
return doc
})
/****************************************************************/
// Sorting
/****************************************************************/

View file

@ -0,0 +1,32 @@
import { I_Blueprint } from "./../../interfaces/I_Blueprint"
import PouchDB from "pouchdb"
import { I_ShortenedDocument } from "src/interfaces/I_OpenedDocument"
/**
* Build a tag list of all know database documents
*/
export const tagListBuildFromBlueprints = async (blueprintList: I_Blueprint[]) => {
let allTags: string[] = []
for (const blueprint of blueprintList) {
const CurrentObjectDB = new PouchDB(blueprint._id)
const dbDocuments = await CurrentObjectDB.allDocs({ include_docs: true })
const docsTagsArray = dbDocuments.rows.map(singleDocument => {
const doc = singleDocument.doc as unknown as I_ShortenedDocument
const tags: string[] = doc.extraFields.find(e => e.id === "tags")?.value
return (tags) || []
})
// @ts-ignore
allTags = [...allTags, ...docsTagsArray] as unknown as string[]
}
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
allTags = allTags.flat()
return [...new Set([
...allTags
])].sort((a, b) => a.localeCompare(b))
}

View file

@ -4,7 +4,7 @@
"baseUrl": ".",
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"suppressImplicitAnyIndexErrors": true,
"suppressImplicitAnyIndexErrors": true
},
"include": [
"src"