diff --git a/src/public/app/widgets/search_options/ancestor.js b/src/public/app/widgets/search_options/ancestor.js index add442990..9372e52fa 100644 --- a/src/public/app/widgets/search_options/ancestor.js +++ b/src/public/app/widgets/search_options/ancestor.js @@ -3,12 +3,43 @@ import noteAutocompleteService from "../../services/note_autocomplete.js"; const TPL = ` - - Ancestor: - - -
- + +
+
Ancestor:
+
+ +
+ +
depth:
+ +
@@ -27,6 +58,7 @@ export default class Ancestor extends AbstractSearchOption { doRender() { const $option = $(TPL); const $ancestor = $option.find('.ancestor'); + const $ancestorDepth = $option.find('.ancestor-depth'); noteAutocompleteService.initNoteAutocomplete($ancestor); $ancestor.on('autocomplete:closed', async () => { @@ -37,12 +69,35 @@ export default class Ancestor extends AbstractSearchOption { } }); + $ancestorDepth.on('change', async () => { + const ancestorDepth = $ancestorDepth.val(); + + if (ancestorDepth) { + await this.setAttribute('label', 'ancestorDepth', ancestorDepth); + } + else { + await this.deleteAttribute('label', 'ancestorDepth'); + } + }); + const ancestorNoteId = this.note.getRelationValue('ancestor'); if (ancestorNoteId !== 'root') { $ancestor.setNote(ancestorNoteId); } + const ancestorDepth = this.note.getLabelValue('ancestorDepth'); + + if (ancestorDepth) { + $ancestorDepth.val(ancestorDepth); + } + return $option; } + + async deleteOption() { + await this.deleteAttribute('label', 'ancestorDepth'); + + await super.deleteOption(); + } } diff --git a/src/routes/api/search.js b/src/routes/api/search.js index 572518a0a..d15916c10 100644 --- a/src/routes/api/search.js +++ b/src/routes/api/search.js @@ -18,6 +18,7 @@ async function search(note) { const searchContext = new SearchContext({ fastSearch: note.hasLabel('fastSearch'), ancestorNoteId: note.getRelationValue('ancestor'), + ancestorDepth: note.getLabelValue('ancestorDepth'), includeArchivedNotes: note.hasLabel('includeArchivedNotes'), orderBy: note.getLabelValue('orderBy'), orderDirection: note.getLabelValue('orderDirection'), diff --git a/src/services/note_cache/entities/note.js b/src/services/note_cache/entities/note.js index 4ea6dca0e..2ff63f715 100644 --- a/src/services/note_cache/entities/note.js +++ b/src/services/note_cache/entities/note.js @@ -368,6 +368,20 @@ class Note { return arr; } + getDistanceToAncestor(ancestorNoteId) { + if (this.noteId === ancestorNoteId) { + return 0; + } + + let minDistance = 999_999; + + for (const parent of this.parents) { + minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1); + } + + return minDistance; + } + decrypt() { if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) { this.title = protectedSessionService.decryptString(this.title); diff --git a/src/services/search/expressions/ancestor.js b/src/services/search/expressions/ancestor.js index 360b71a40..086da4833 100644 --- a/src/services/search/expressions/ancestor.js +++ b/src/services/search/expressions/ancestor.js @@ -6,10 +6,11 @@ const log = require('../../log'); const noteCache = require('../../note_cache/note_cache'); class AncestorExp extends Expression { - constructor(ancestorNoteId) { + constructor(ancestorNoteId, ancestorDepth) { super(); this.ancestorNoteId = ancestorNoteId; + this.ancestorDepthComparator = this.getComparator(ancestorDepth); } execute(inputNoteSet, executionContext) { @@ -21,7 +22,45 @@ class AncestorExp extends Expression { return new NoteSet([]); } - return new NoteSet(ancestorNote.subtreeNotes).intersection(inputNoteSet); + const subTreeNoteSet = new NoteSet(ancestorNote.subtreeNotes).intersection(inputNoteSet); + + if (!this.ancestorDepthComparator) { + return subTreeNoteSet; + } + + const depthConformingNoteSet = new NoteSet([]); + + for (const note of subTreeNoteSet.notes) { + const distance = note.getDistanceToAncestor(ancestorNote.noteId); + + if (this.ancestorDepthComparator(distance)) { + depthConformingNoteSet.add(note); + } + } + + return depthConformingNoteSet; + } + + getComparator(depthCondition) { + if (!depthCondition) { + return null; + } + + const comparedDepth = parseInt(depthCondition.substr(2)); + + if (depthCondition.startsWith("eq")) { + return depth => depth === comparedDepth; + } + else if (depthCondition.startsWith("gt")) { + return depth => depth > comparedDepth; + } + else if (depthCondition.startsWith("lt")) { + return depth => depth < comparedDepth; + } + else { + log.error(`Unrecognized depth condition value ${depthCondition}`); + return null; + } } } diff --git a/src/services/search/search_context.js b/src/services/search/search_context.js index d64c4edb3..01691e472 100644 --- a/src/services/search/search_context.js +++ b/src/services/search/search_context.js @@ -4,6 +4,7 @@ class SearchContext { constructor(params = {}) { this.fastSearch = !!params.fastSearch; this.ancestorNoteId = params.ancestorNoteId; + this.ancestorDepth = params.ancestorDepth; this.includeArchivedNotes = !!params.includeArchivedNotes; this.orderBy = params.orderBy; this.orderDirection = params.orderDirection; diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.js index a8aa2a32f..1cf0982ac 100644 --- a/src/services/search/services/parse.js +++ b/src/services/search/services/parse.js @@ -410,7 +410,7 @@ function getExpression(tokens, searchContext, level = 0) { function parse({fulltextTokens, expressionTokens, searchContext}) { let exp = AndExp.of([ searchContext.includeArchivedNotes ? null : new PropertyComparisonExp(searchContext, "isarchived", buildComparator("=", "false")), - searchContext.ancestorNoteId ? new AncestorExp(searchContext.ancestorNoteId) : null, + searchContext.ancestorNoteId ? new AncestorExp(searchContext.ancestorNoteId, searchContext.ancestorDepth) : null, getFulltext(fulltextTokens, searchContext), getExpression(expressionTokens, searchContext) ]);