From 90d33f56c3323cfb11a09858ab9e0979fb1be88b Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 5 Dec 2020 23:00:28 +0100 Subject: [PATCH] implemented "search in subtree" --- package-lock.json | 54 +++++++++---------- package.json | 8 +-- src/public/app/services/date_notes.js | 15 +++++- .../app/services/dialog_command_executor.js | 12 ++++- src/public/app/services/entrypoints.js | 15 ------ src/public/app/services/tree_context_menu.js | 2 + src/public/app/widgets/note_tree.js | 4 +- src/public/app/widgets/search_definition.js | 12 +++-- src/routes/api/search.js | 26 +-------- src/routes/routes.js | 1 - src/services/search/expressions/sub_tree.js | 28 ++++++++++ src/services/search/search_context.js | 1 + src/services/search/services/parse.js | 2 + 13 files changed, 98 insertions(+), 82 deletions(-) create mode 100644 src/services/search/expressions/sub_tree.js diff --git a/package-lock.json b/package-lock.json index ba2193e24..5ecad559a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "trilium", - "version": "0.45.5", + "version": "0.45.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2044,9 +2044,9 @@ "dev": true }, "commonmark": { - "version": "0.29.2", - "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.29.2.tgz", - "integrity": "sha512-spe43MvEIaPpHss1T7z4yQaFQfLGmMu+yvCwv6xqhELIwkG/ZGgDpxOPzKxnuYzYT2c+aziCCc8m2rBVLA7jUA==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.29.3.tgz", + "integrity": "sha512-fvt/NdOFKaL2gyhltSy6BC4LxbbxbnPxBMl923ittqO/JBM0wQHaoYZliE4tp26cRxX/ZZtRsJlZzQrVdUkXAA==", "requires": { "entities": "~2.0", "mdurl": "~1.0.1", @@ -2358,9 +2358,9 @@ } }, "dayjs": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.6.tgz", - "integrity": "sha512-HngNLtPEBWRo8EFVmHFmSXAjtCX8rGNqeXQI0Gh7wCTSqwaKgPIDqu9m07wABVopNwzvOeCb+2711vQhDlcIXw==" + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.7.tgz", + "integrity": "sha512-IC877KBdMhBrCfBfJXHQlo0G8keZ0Opy7YIIq5QKtUbCuHMzim8S4PyiVK4YmihI3iOF9lhfUBW4AQWHTR5WHA==" }, "debug": { "version": "4.1.1", @@ -2910,9 +2910,9 @@ } }, "electron-osx-sign": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.17.tgz", - "integrity": "sha512-wUJPmZJQCs1zgdlQgeIpRcvrf7M5/COQaOV68Va1J/SgmWx5KL2otgg+fAae7luw6qz9R8Gvu/Qpe9tAOu/3xQ==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.5.0.tgz", + "integrity": "sha512-icoRLHzFz/qxzDh/N4Pi2z4yVHurlsCAYQvsCSG7fCedJ4UJXBS6PoQyGH71IfcqKupcKeK7HX/NkyfG+v6vlQ==", "dev": true, "requires": { "bluebird": "^3.5.0", @@ -2944,16 +2944,16 @@ } }, "electron-packager": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-15.1.0.tgz", - "integrity": "sha512-THNm4bz1DfvR9f0g51+NjuAYELflM8+1vhQ/iv/G8vyZNKzSMuFd5doobngQKq3rRsLdPNZVnGqDdgS884d7Og==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-15.2.0.tgz", + "integrity": "sha512-BaklTBRQy1JTijR3hi8XxHf/uo76rHbDCNM/eQHSblzE9C0NoNfOe86nPxB7y1u2jwlqoEJ4zFiHpTFioKGGRA==", "dev": true, "requires": { "@electron/get": "^1.6.0", "asar": "^3.0.0", "debug": "^4.0.1", "electron-notarize": "^1.0.0", - "electron-osx-sign": "^0.4.11", + "electron-osx-sign": "^0.5.0", "extract-zip": "^2.0.0", "filenamify": "^4.1.0", "fs-extra": "^9.0.0", @@ -2965,7 +2965,7 @@ "rcedit": "^2.0.0", "resolve": "^1.1.6", "semver": "^7.1.3", - "yargs-parser": "^19.0.1" + "yargs-parser": "^20.0.0" }, "dependencies": { "asar": { @@ -3001,12 +3001,6 @@ "requires": { "pump": "^3.0.0" } - }, - "yargs-parser": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-19.0.1.tgz", - "integrity": "sha512-2UuJKZmPN9S9/0s3FSCG3aNUSyC/qz56oJsMZG0NV2B44QxTXaNySp4xXW10CizmUs0DXgPY0y114dOGLvtYHg==", - "dev": true } } }, @@ -3674,9 +3668,9 @@ "dev": true }, "filenamify": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.1.0.tgz", - "integrity": "sha512-KQV/uJDI9VQgN7sHH1Zbk6+42cD6mnQ2HONzkXUfPJ+K2FC8GZ1dpewbbHw0Sz8Tf5k3EVdHVayM4DoAwWlmtg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.2.0.tgz", + "integrity": "sha512-pkgE+4p7N1n7QieOopmn3TqJaefjdWXwEkj2XLZJLKfOgcQKkn11ahvGNgTD8mLggexLiDFQxeTs14xVU22XPA==", "dev": true, "requires": { "filename-reserved-regex": "^2.0.0", @@ -3854,9 +3848,9 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { "ms": "^2.1.1" @@ -7951,9 +7945,9 @@ } }, "ws": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", - "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==" + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", + "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==" }, "xdg-basedir": { "version": "4.0.0", diff --git a/package.json b/package.json index 669f5ed21..b9b5397f7 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,10 @@ "better-sqlite3": "7.1.1", "body-parser": "1.19.0", "cls-hooked": "4.2.2", - "commonmark": "0.29.2", + "commonmark": "0.29.3", "cookie-parser": "1.4.5", "csurf": "1.11.0", - "dayjs": "1.9.6", + "dayjs": "1.9.7", "ejs": "3.1.5", "electron-debug": "3.1.0", "electron-dl": "3.0.2", @@ -71,7 +71,7 @@ "turndown": "7.0.0", "turndown-plugin-gfm": "1.0.2", "unescape": "1.0.1", - "ws": "7.4.0", + "ws": "7.4.1", "yauzl": "2.10.0", "yazl": "2.5.1" }, @@ -79,7 +79,7 @@ "cross-env": "7.0.3", "electron": "9.3.5", "electron-builder": "22.9.1", - "electron-packager": "15.1.0", + "electron-packager": "15.2.0", "electron-rebuild": "2.3.4", "esm": "3.2.25", "jasmine": "3.6.3", diff --git a/src/public/app/services/date_notes.js b/src/public/app/services/date_notes.js index d31273ad0..7b7a54a2d 100644 --- a/src/public/app/services/date_notes.js +++ b/src/public/app/services/date_notes.js @@ -1,5 +1,6 @@ import treeCache from "./tree_cache.js"; import server from "./server.js"; +import ws from "./ws.js"; /** @return {NoteShort} */ async function getInboxNote() { @@ -42,10 +43,20 @@ async function createSqlConsole() { } /** @return {NoteShort} */ -async function createSearchNote() { +async function createSearchNote(subTreeNoteId = null) { const note = await server.post('search-note'); - return await treeCache.getNote(note.noteId); + if (subTreeNoteId) { + await server.put(`notes/${note.noteId}/attributes`, [ + { type: 'label', name: 'subTreeNoteId', value: subTreeNoteId } + ]); + } + + await ws.waitForMaxKnownEntityChangeId(); + + const noteShort = await treeCache.getNote(note.noteId); + + return noteShort; } export default { diff --git a/src/public/app/services/dialog_command_executor.js b/src/public/app/services/dialog_command_executor.js index 408cebf9e..734fdf2ef 100644 --- a/src/public/app/services/dialog_command_executor.js +++ b/src/public/app/services/dialog_command_executor.js @@ -1,7 +1,7 @@ import Component from "../widgets/component.js"; import appContext from "./app_context.js"; import dateNoteService from "../services/date_notes.js"; -import noteCreateService from "../services/note_create.js"; +import treeService from "../services/tree.js"; export default class DialogCommandExecutor extends Component { jumpToNoteCommand() { @@ -75,6 +75,16 @@ export default class DialogCommandExecutor extends Component { appContext.triggerCommand('focusOnSearchDefinition', {tabId: tabContext.tabId}); } + async searchInSubtreeCommand({notePath}) { + const noteId = treeService.getNoteIdFromNotePath(notePath); + + const searchNote = await dateNoteService.createSearchNote(noteId); + + const tabContext = await appContext.tabManager.openTabWithNote(searchNote.noteId, true); + + appContext.triggerCommand('focusOnSearchDefinition', {tabId: tabContext.tabId}); + } + showBackendLogCommand() { import("../dialogs/backend_log.js").then(d => d.showDialog()); } diff --git a/src/public/app/services/entrypoints.js b/src/public/app/services/entrypoints.js index 842dd0612..20011a54d 100644 --- a/src/public/app/services/entrypoints.js +++ b/src/public/app/services/entrypoints.js @@ -176,21 +176,6 @@ export default class Entrypoints extends Component { } } - async searchForResultsCommand({searchText}) { - const response = await server.get('search/' + encodeURIComponent(searchText) + '?includeNoteContent=true&excludeArchived=true&fuzzyAttributeSearch=false'); - - if (!response.success) { - toastService.showError("Search failed: " + response.message, 10000); - // even in this case we'll show the results - } - - this.triggerEvent('searchResults', {results: response.results}); - - // have at least some feedback which is good especially in situations - // when the result list does not change with a query - toastService.showMessage("Search finished successfully."); - } - async switchToDesktopVersionCommand() { utils.setCookie('trilium-device', 'desktop'); diff --git a/src/public/app/services/tree_context_menu.js b/src/public/app/services/tree_context_menu.js index 16c2939e4..99e0b93c9 100644 --- a/src/public/app/services/tree_context_menu.js +++ b/src/public/app/services/tree_context_menu.js @@ -129,6 +129,8 @@ class TreeContextMenu { }); } else { + console.log("Triggering", command, notePath); + this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath}); } } diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js index ca7653f2c..bdbfd9a09 100644 --- a/src/public/app/widgets/note_tree.js +++ b/src/public/app/widgets/note_tree.js @@ -1220,7 +1220,9 @@ export default class NoteTreeWidget extends TabAwareWidget { for (const action of actions) { for (const shortcut of action.effectiveShortcuts) { hotKeyMap[utils.normalizeShortcut(shortcut)] = node => { - this.triggerCommand(action.actionName, {node}); + const notePath = treeService.getNotePath(node); + + this.triggerCommand(action.actionName, {node, notePath}); return false; } diff --git a/src/public/app/widgets/search_definition.js b/src/public/app/widgets/search_definition.js index d6b352c5b..134445a7e 100644 --- a/src/public/app/widgets/search_definition.js +++ b/src/public/app/widgets/search_definition.js @@ -96,13 +96,13 @@ export default class SearchDefinitionWidget extends TabAwareWidget { async updateSearch() { const searchString = this.$searchString.val(); - const subNoteId = this.$limitSearchToSubtree.getSelectedNoteId(); + const subTreeNoteId = this.$limitSearchToSubtree.getSelectedNoteId(); const includeNoteContent = this.$searchWithinNoteContent.is(":checked"); await server.put(`notes/${this.noteId}/attributes`, [ { type: 'label', name: 'searchString', value: searchString }, { type: 'label', name: 'includeNoteContent', value: includeNoteContent ? 'true' : 'false' }, - subNoteId ? { type: 'label', name: 'subTreeNoteId', value: subNoteId } : undefined, + subTreeNoteId ? { type: 'label', name: 'subTreeNoteId', value: subTreeNoteId } : undefined, ].filter(it => !!it)); if (this.note.title.startsWith('Search: ')) { @@ -122,7 +122,13 @@ export default class SearchDefinitionWidget extends TabAwareWidget { this.$component.show(); this.$searchString.val(this.note.getLabelValue('searchString')); this.$searchWithinNoteContent.prop('checked', this.note.getLabelValue('includeNoteContent') === 'true'); - this.$limitSearchToSubtree.val(this.note.getLabelValue('subTreeNoteId')); + + const subTreeNoteId = this.note.getLabelValue('subTreeNoteId'); + const subTreeNote = subTreeNoteId ? await treeCache.getNote(subTreeNoteId, true) : null; + + this.$limitSearchToSubtree + .val(subTreeNote ? subTreeNote.title : "") + .setSelectedNotePath(subTreeNoteId); this.refreshResults(); // important specifically when this search note was not yet refreshed } diff --git a/src/routes/api/search.js b/src/routes/api/search.js index 53bbcd226..c24067c93 100644 --- a/src/routes/api/search.js +++ b/src/routes/api/search.js @@ -6,30 +6,6 @@ const log = require('../../services/log'); const scriptService = require('../../services/script'); const searchService = require('../../services/search/services/search'); -function searchNotes(req) { - const searchContext = new SearchContext({ - includeNoteContent: req.query.includeNoteContent === 'true', - excludeArchived: req.query.excludeArchived === 'true', - fuzzyAttributeSearch: req.query.fuzzyAttributeSearch === 'true' - }); - - const {count, results} = searchService.searchTrimmedNotes(req.params.searchString, searchContext); - - try { - return { - success: !searchContext.hasError(), - message: searchContext.getError(), - count, - results - } - } - catch { - return { - success: false - } - } -} - async function searchFromNote(req) { const note = repository.getNote(req.params.noteId); @@ -58,6 +34,7 @@ async function searchFromNote(req) { else if (searchString) { const searchContext = new SearchContext({ includeNoteContent: note.getLabelValue('includeNoteContent') === 'true', + subTreeNoteId: note.getLabelValue('subTreeNoteId'), excludeArchived: true, fuzzyAttributeSearch: false }); @@ -197,7 +174,6 @@ function formatValue(val) { } module.exports = { - searchNotes, searchFromNote, getRelatedNotes }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 1f42e842e..588111d13 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -254,7 +254,6 @@ function register(app) { route(POST, '/api/sender/image', [auth.checkToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler); route(POST, '/api/sender/note', [auth.checkToken], senderRoute.saveNote, apiResultHandler); - apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes); apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote); apiRoute(POST, '/api/search-related', searchRoute.getRelatedNotes); diff --git a/src/services/search/expressions/sub_tree.js b/src/services/search/expressions/sub_tree.js new file mode 100644 index 000000000..8c24dd9c1 --- /dev/null +++ b/src/services/search/expressions/sub_tree.js @@ -0,0 +1,28 @@ +"use strict"; + +const Expression = require('./expression'); +const NoteSet = require('../note_set'); +const log = require('../../log'); +const noteCache = require('../../note_cache/note_cache'); + +class SubTreeExp extends Expression { + constructor(subTreeNoteId) { + super(); + + this.subTreeNoteId = subTreeNoteId; + } + + execute(inputNoteSet, searchContext) { + const subTreeNote = noteCache.notes[this.subTreeNoteId]; + + if (!subTreeNote) { + log.error(`Subtree note '${this.subTreeNoteId}' was not not found.`); + + return new NoteSet([]); + } + + return new NoteSet(subTreeNote.subtreeNotes).intersection(inputNoteSet); + } +} + +module.exports = SubTreeExp; diff --git a/src/services/search/search_context.js b/src/services/search/search_context.js index 127cd4dd6..976adbfa8 100644 --- a/src/services/search/search_context.js +++ b/src/services/search/search_context.js @@ -3,6 +3,7 @@ class SearchContext { constructor(params = {}) { this.includeNoteContent = !!params.includeNoteContent; + this.subTreeNoteId = params.subTreeNoteId; this.excludeArchived = !!params.excludeArchived; this.fuzzyAttributeSearch = !!params.fuzzyAttributeSearch; this.highlightedTokens = []; diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.js index fefbb72ad..3873ac22c 100644 --- a/src/services/search/services/parse.js +++ b/src/services/search/services/parse.js @@ -15,6 +15,7 @@ const NoteCacheFulltextExp = require('../expressions/note_cache_flat_text.js'); const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext.js'); const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext.js'); const OrderByAndLimitExp = require('../expressions/order_by_and_limit.js'); +const SubTreeExp = require("../expressions/sub_tree.js"); const buildComparator = require('./build_comparator.js'); const ValueExtractor = require('../value_extractor.js'); @@ -409,6 +410,7 @@ function getExpression(tokens, searchContext, level = 0) { function parse({fulltextTokens, expressionTokens, searchContext}) { return AndExp.of([ searchContext.excludeArchived ? new PropertyComparisonExp("isarchived", buildComparator("=", "false")) : null, + searchContext.subTreeNoteId ? new SubTreeExp(searchContext.subTreeNoteId) : null, getFulltext(fulltextTokens, searchContext), getExpression(expressionTokens, searchContext) ]);