Increased power of tree searching
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function escapeRegex(string) {
|
||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '');
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
RegExp.escape = function(s) {
|
||||
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
};
|
||||
@@ -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,
|
||||
}),
|
||||
];
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user