Increased power of tree searching

This commit is contained in:
Stefan Zermatten
2023-06-26 14:45:19 +02:00
parent d4cac831e6
commit 7562e29fac
8 changed files with 205 additions and 63 deletions

View File

@@ -1,4 +1,4 @@
import { union, difference, sortBy, findLast } from 'lodash';
import { union, difference, sortBy, findLast, intersection } from 'lodash';
export function nodeArrayToTree(nodes) {
// Store a dict and list of all the nodes
@@ -83,9 +83,15 @@ export default function nodesToTree({
docs.forEach(doc => {
ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id));
});
// Remove the IDs of docs we have already found
// Get all the docs that are also ancestors and mark them
docs.forEach(doc => {
if (ancestorIds.includes(doc._id)) {
doc._ancestorOfMatchedDocument = true;
}
});
// Remove the ancestor IDs of docs we have already found
ancestorIds = difference(ancestorIds, docIds);
// Get the docs from the collection, don't worry about `removed` docs,
// Get the ancestor docs from the collection, don't worry about `removed` docs,
// if their descendant was not removed, neither are they
ancestors = collection.find({ _id: { $in: ancestorIds } }).map(doc => {
// Mark that the nodes are ancestors of the found nodes

View File

@@ -1,3 +1,3 @@
export default function escapeRegex(string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '');
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

View File

@@ -1,19 +1,111 @@
<template lang="html">
<v-combobox
v-model="filterTerms"
:items="filterOptions"
prepend-inner-icon="mdi-magnify"
hide-no-data
hide-selected
multiple
clearable
small-chips
deletable-chips
/>
<v-menu
v-model="menu"
:close-on-content-click="false"
>
<template #activator="{ on, attrs }">
<v-btn
v-bind="attrs"
icon
v-on="on"
>
<v-badge
:content="numFilters"
:value="numFilters"
color="primary"
overlap
>
<v-icon>mdi-magnify</v-icon>
</v-badge>
</v-btn>
</template>
<v-card>
<v-card-title>
Search
</v-card-title>
<v-card-text>
<v-select
v-model="typeFilterInput"
outlined
label="Type"
:items="filterOptions"
multiple
clearable
small-chips
deletable-chips
/>
<v-slide-x-transition group>
<div
v-for="(fieldFilter, index) in fieldFilters"
:key="index"
class="d-flex"
>
<v-text-field
v-model="fieldFilter.field"
class="text--mono"
label="Field"
outlined
/>
<v-text-field
v-model="fieldFilter.value"
label="Text"
class="ml-2"
outlined
/>
<v-btn
v-if="fieldFilters.length > 1"
icon
@click="fieldFilters.splice(index, 1)"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
</v-slide-x-transition>
<div
v-if="fieldFilters.length < 5"
class="d-flex"
>
<v-spacer />
<v-btn
icon
@click="fieldFilters.push({name: '', value: undefined})"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
<v-card-actions>
<v-btn
text
@click="
fieldFilters = [{field: 'name', value: undefined}];
typeFilterInput = [];
menu = false;
"
>
<v-icon left>
mdi-close
</v-icon>
Clear
</v-btn>
<v-spacer />
<v-btn
text
color="primary"
@click="menu = false"
>
Find
</v-btn>
</v-card-actions>
</v-card-text>
</v-card>
</v-menu>
</template>
<script lang="js">
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import escapeRegex from '/imports/api/utility/escapeRegex.js';
const filterOptions = [];
for (let key in PROPERTIES) {
if (key === 'reference') continue;
@@ -31,39 +123,62 @@ export default {
},
},
data(){return {
filterTerms: [],
typeFilterInput: [],
fieldFilters: [{field: 'name', value: undefined}],
filterOptions,
menu: false,
}},
computed: {
filter(){
if (!this.filterTerms.length) return;
let typeFilters = [];
let nameFilters = [];
this.filterTerms.forEach(filter => {
if (filter.value){
typeFilters.push(filter.value);
filter() {
let filter = undefined;
if (this.typeFilterInput?.length) {
filter = filter || {};
filter.type = {$in: this.typeFilterInput};
}
this.fieldFilters?.forEach(fieldFilter => {
if (!fieldFilter.field || !fieldFilter.value) return;
const search = { $regex: escapeRegex(fieldFilter.value), '$options': 'i' };
filter = filter || {};
if (fieldFilter.field.includes('.')) {
// The user used dot notation, search exactly where they are looking
filter[fieldFilter.field] = search;
} else {
// escape string
let term = filter.replace( /[-/\\^$*+?.()|[\]{}]/g, '\\$&' );
var reg = new RegExp( '.*' + term + '.*', 'i' );
nameFilters.push(reg)
// No dot notation, search fields and their likely sub-fields
filter.$and = filter.$and || [];
filter.$and.push({
$or: [
{ [fieldFilter.field]: search },
{ [fieldFilter.field + '.calculation']: search },
{ [fieldFilter.field + '.text']: search },
],
});
}
});
let filter = {};
if (typeFilters.length){
filter.type = {$in: typeFilters};
}
if (nameFilters.length){
filter.name = {$in: nameFilters};
}
return filter;
},
},
watch:{
filter(value){
this.$emit('input', value);
extraFields() {
let extraFields = [];
this.fieldFilters?.forEach(fieldFilter => {
if (!fieldFilter.field || !fieldFilter.value) return;
extraFields.push(fieldFilter.field);
});
return extraFields;
},
numFilters() {
let numFilters = 0;
if (this.typeFilterInput?.length) numFilters += 1;
numFilters += this.extraFields.length;
return numFilters;
}
}
},
watch: {
menu(val) {
if (!val) {
this.$emit('input', this.filter);
this.$emit('extra-fields-changed', this.extraFields);
}
}
},
}
</script>

View File

@@ -11,9 +11,14 @@
<template slot="tree">
<v-toolbar
flat
dense
dark
style="flex-grow: 0;"
>
<tree-search-input
ref="searchBox"
v-model="filter"
class="mx-4"
/>
<v-spacer />
<v-switch
v-if="context.editPermission !== false"
@@ -23,12 +28,6 @@
:disabled="organizeDisabled"
style="flex-grow: 0; height: 32px;"
/>
<tree-search-input
ref="searchBox"
slot="extension"
v-model="filter"
class="mx-4"
/>
</v-toolbar>
<creature-properties-tree
class="pt-2 flex"

View File

@@ -17,6 +17,12 @@
:dark="isToolbarDark"
:light="!isToolbarDark"
>
<tree-search-input
ref="searchBox"
v-model="filter"
class="mx-4"
@extra-fields-changed="val => extraFields = val"
/>
<v-spacer />
<v-switch
v-if="!libraryId || canEditLibrary"
@@ -25,15 +31,8 @@
class="mx-3"
style="flex-grow: 0; height: 32px;"
/>
<tree-search-input
ref="searchBox"
slot="extension"
v-model="filter"
class="mx-4"
/>
<insert-library-node-button
v-if="libraryId && canEditLibrary"
slot="extension"
style="bottom: -24px"
fab
:library-id="libraryId"
@@ -49,6 +48,7 @@
:library-id="libraryId"
:organize-mode="organize"
:selected-node="selectedNode"
:extra-fields="extraFields"
should-subscribe
:filter="filter"
@selected="clickNode"
@@ -114,6 +114,7 @@ export default {
organize: false,
selectedNodeId: undefined,
filter: undefined,
extraFields: [],
};},
computed: {
isToolbarDark(){
@@ -121,7 +122,7 @@ export default {
this.selectedNode && this.selectedNode.color ||
getThemeColor('secondary')
);
}
},
},
watch:{
selectedNode(val){

View File

@@ -50,6 +50,10 @@ export default {
type: Object,
default: undefined,
},
extraFields: {
type: Array,
default: undefined,
},
},
data() {
return {
@@ -75,7 +79,7 @@ export default {
$subscribe: {
'libraryNodes'() {
if (this.slowShouldSubscribe) {
return [this.libraryId];
return [this.libraryId, this.extraFields];
} else {
return [];
}

View File

@@ -1,3 +0,0 @@
RegExp.escape = function(s) {
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
};

View File

@@ -203,22 +203,42 @@ let libraryIdSchema = new SimpleSchema({
},
});
Meteor.publish('libraryNodes', function (libraryId) {
const extraFieldsSchema = new SimpleSchema({
extraFields: {
type: Array,
optional: true,
},
'extraFields.$': {
type: String,
},
});
Meteor.publish('libraryNodes', function (libraryId, extraFields) {
if (!libraryId) return [];
libraryIdSchema.validate({ libraryId });
try {
libraryIdSchema.validate({ libraryId });
extraFieldsSchema.validate({ extraFields });
} catch (e) {
return this.error(e);
}
this.autorun(function () {
let userId = this.userId;
let library = Libraries.findOne(libraryId);
try { assertViewPermission(library, userId) }
catch (e) {
try {
assertViewPermission(library, userId)
} catch (e) {
return this.error(e);
}
const fields = { ...LIBRARY_NODE_TREE_FIELDS };
extraFields?.forEach(field => {
fields[field] = 1;
});
return [
LibraryNodes.find({
'ancestors.id': libraryId,
}, {
sort: { order: 1 },
fields: LIBRARY_NODE_TREE_FIELDS,
fields,
}),
];
});