diff --git a/src/public/app/layouts/desktop_main_window_layout.js b/src/public/app/layouts/desktop_main_window_layout.js index 7b4299700..2f8e4bb59 100644 --- a/src/public/app/layouts/desktop_main_window_layout.js +++ b/src/public/app/layouts/desktop_main_window_layout.js @@ -20,7 +20,7 @@ import NoteInfoWidget from "../widgets/collapsible_widgets/note_info.js"; import CalendarWidget from "../widgets/collapsible_widgets/calendar.js"; import LinkMapWidget from "../widgets/collapsible_widgets/link_map.js"; import NoteRevisionsWidget from "../widgets/collapsible_widgets/note_revisions.js"; -import SimilarNotesWidget from "../widgets/collapsible_widgets/similar_notes.js"; +import SimilarNotesWidget from "../widgets/similar_notes.js"; import WhatLinksHereWidget from "../widgets/collapsible_widgets/what_links_here.js"; import SidePaneToggles from "../widgets/side_pane_toggles.js"; import EditedNotesWidget from "../widgets/collapsible_widgets/edited_notes.js"; @@ -138,6 +138,7 @@ export default class DesktopMainWindowLayout { ) .child(new TabCachingWidget(() => new AttributeListWidget())) .child(new TabCachingWidget(() => new NoteDetailWidget())) + .child(new TabCachingWidget(() => new SimilarNotesWidget())) .child(...this.customWidgets.get('center-pane')) ) .child(new SidePaneContainer('right') @@ -148,7 +149,6 @@ export default class DesktopMainWindowLayout { .child(new TabCachingWidget(() => new EditedNotesWidget())) .child(new TabCachingWidget(() => new LinkMapWidget())) .child(new TabCachingWidget(() => new NoteRevisionsWidget())) - .child(new TabCachingWidget(() => new SimilarNotesWidget())) .child(new TabCachingWidget(() => new WhatLinksHereWidget())) .child(...this.customWidgets.get('right-pane')) ) diff --git a/src/public/app/widgets/attribute_list.js b/src/public/app/widgets/attribute_list.js index 6cffc2c05..70d30e168 100644 --- a/src/public/app/widgets/attribute_list.js +++ b/src/public/app/widgets/attribute_list.js @@ -56,7 +56,7 @@ const TPL = ` cursor: pointer; } - .attr-expander:not(.error):hover hr { + .attr-expander:hover hr { border-color: var(--main-text-color); } diff --git a/src/public/app/widgets/collapsible_widgets/similar_notes.js b/src/public/app/widgets/collapsible_widgets/similar_notes.js deleted file mode 100644 index 29561dfbb..000000000 --- a/src/public/app/widgets/collapsible_widgets/similar_notes.js +++ /dev/null @@ -1,96 +0,0 @@ -import CollapsibleWidget from "../collapsible_widget.js"; -import linkService from "../../services/link.js"; -import server from "../../services/server.js"; -import treeCache from "../../services/tree_cache.js"; - -const TPL = ` -
- - -
-
-`; - -export default class SimilarNotesWidget extends CollapsibleWidget { - get widgetTitle() { return "Similar notes"; } - - async doRenderBody() { - this.$body.html(TPL); - - this.$similarNotesContent = this.$body.find(".similar-notes-content"); - } - - get help() { - return { - title: "This list contains notes which might be similar to the current note based on textual similarity of note title." - }; - } - - noteSwitched() { - const noteId = this.noteId; - - this.$similarNotesContent.empty(); - - // avoid executing this expensive operation multiple times when just going through notes (with keyboard especially) - // until the users settles on a note - setTimeout(() => { - if (this.noteId === noteId) { - this.refresh(); - } - }, 1000); - } - - async refreshWithNote(note) { - // remember which title was when we found the similar notes - this.title = note.title; - - const similarNotes = await server.get('similar-notes/' + this.noteId); - - if (similarNotes.length === 0) { - this.$similarNotesContent.text("No similar notes found ..."); - return; - } - - const noteIds = similarNotes.flatMap(note => note.notePath); - - await treeCache.getNotes(noteIds, true); // preload all at once - - const $list = $('
'); - - for (const similarNote of similarNotes) { - const note = await treeCache.getNote(similarNote.noteId, true); - - if (!note) { - continue; - } - - const $item = (await linkService.createNoteLink(similarNote.notePath.join("/"))) - .css("font-size", 24 * similarNote.coeff); - - $list.append($item); - } - - this.$similarNotesContent.empty().append($list); - } - - entitiesReloadedEvent({loadResults}) { - if (this.note && this.title !== this.note.title) { - this.rendered = false; - - this.refresh(); - } - } -} diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js index 9d9ab7e29..b2406daef 100644 --- a/src/public/app/widgets/note_tree.js +++ b/src/public/app/widgets/note_tree.js @@ -51,7 +51,7 @@ const TPL = ` .tree-settings-button { position: absolute; top: 10px; - right: 20px; + right: 10px; z-index: 100; } diff --git a/src/public/app/widgets/similar_notes.js b/src/public/app/widgets/similar_notes.js new file mode 100644 index 000000000..06eb31fca --- /dev/null +++ b/src/public/app/widgets/similar_notes.js @@ -0,0 +1,163 @@ +import linkService from "../services/link.js"; +import server from "../services/server.js"; +import treeCache from "../services/tree_cache.js"; +import TabAwareWidget from "./tab_aware_widget.js"; +import options from "../services/options.js"; + +const TPL = ` +
+ + +
+
+ +
+ +
+
+ +
+
+`; + +export default class SimilarNotesWidget extends TabAwareWidget { + doRender() { + this.$widget = $(TPL); + this.overflowing(); + + this.$similarNotesWrapper = this.$widget.find(".similar-notes-wrapper"); + this.$similarNotesExpanderText = this.$widget.find(".similar-notes-expander-text"); + + this.$expander = this.$widget.find('.similar-notes-expander'); + this.$expander.on('click', async () => { + const collapse = this.$similarNotesWrapper.is(":visible"); + + await options.save('similarNotesExpanded', !collapse); + + this.triggerEvent(`similarNotesCollapsedStateChanged`, {collapse}); + }); + + return this.$widget; + } + + noteSwitched() { + const noteId = this.noteId; + + this.toggleInt(false); + this.$similarNotesWrapper.hide(); // we'll open it in refresh() if needed + + // avoid executing this expensive operation multiple times when just going through notes (with keyboard especially) + // until the users settles on a note + setTimeout(() => { + if (this.noteId === noteId) { + this.refresh(); + } + }, 1000); + } + + async refreshWithNote(note) { + // remember which title was when we found the similar notes + this.title = note.title; + + const similarNotes = await server.get('similar-notes/' + this.noteId); + + this.toggleInt(similarNotes.length > 0); + + if (similarNotes.length === 0) { + return; + } + + if (options.is('similarNotesExpanded')) { + this.$similarNotesWrapper.show(); + } + + this.$similarNotesExpanderText.text(`${similarNotes.length} similar note${similarNotes.length === 1 ? '': "s"}`); + + const noteIds = similarNotes.flatMap(note => note.notePath); + + await treeCache.getNotes(noteIds, true); // preload all at once + + const $list = $('
'); + + for (const similarNote of similarNotes) { + const note = await treeCache.getNote(similarNote.noteId, true); + + if (!note) { + continue; + } + + const $item = (await linkService.createNoteLink(similarNote.notePath.join("/"))) + .css("font-size", 24 * similarNote.coeff); + + $list.append($item); + } + + this.$similarNotesWrapper.empty().append($list); + } + + entitiesReloadedEvent({loadResults}) { + if (this.note && this.title !== this.note.title) { + this.rendered = false; + + this.refresh(); + } + } + + /** + * This event is used to synchronize collapsed state of all the tab-cached widgets since they are all rendered + * separately but should behave uniformly for the user. + */ + similarNotesCollapsedStateChangedEvent({collapse}) { + if (collapse) { + this.$similarNotesWrapper.slideUp(200); + } else { + this.$similarNotesWrapper.slideDown(200); + } + } +} diff --git a/src/public/app/widgets/standard_top_widget.js b/src/public/app/widgets/standard_top_widget.js index d6382c9bc..8d16f3a75 100644 --- a/src/public/app/widgets/standard_top_widget.js +++ b/src/public/app/widgets/standard_top_widget.js @@ -82,7 +82,7 @@ export default class StandardTopWidget extends BasicWidget { this.$leaveProtectedSessionButton = this.$widget.find(".leave-protected-session-button"); this.$leaveProtectedSessionButton.on('click', protectedSessionService.leaveProtectedSession); - return this.$widget + return this.$widget; } protectedSessionStartedEvent() { diff --git a/src/routes/api/options.js b/src/routes/api/options.js index 8bd5579aa..24eae0a91 100644 --- a/src/routes/api/options.js +++ b/src/routes/api/options.js @@ -39,7 +39,8 @@ const ALLOWED_OPTIONS = new Set([ 'rightPaneVisible', 'nativeTitleBarVisible', 'attributeListExpanded', - 'promotedAttributesExpanded' + 'promotedAttributesExpanded', + 'similarNotesExpanded' ]); function getOptions() { diff --git a/src/services/note_cache/note_cache_service.js b/src/services/note_cache/note_cache_service.js index a586a8bc8..8b563827b 100644 --- a/src/services/note_cache/note_cache_service.js +++ b/src/services/note_cache/note_cache_service.js @@ -229,7 +229,7 @@ function findSimilarNotes(noteId) { results.sort((a, b) => a.coeff > b.coeff ? -1 : 1); - return results.length > 50 ? results.slice(0, 50) : results; + return results.length > 50 ? results.slice(0, 200) : results; } /** diff --git a/src/services/options_init.js b/src/services/options_init.js index d17637364..4fe655a18 100644 --- a/src/services/options_init.js +++ b/src/services/options_init.js @@ -86,7 +86,8 @@ const defaultOptions = [ { name: 'hideArchivedNotes_main', value: 'false', isSynced: false }, { name: 'hideIncludedImages_main', value: 'true', isSynced: false }, { name: 'attributeListExpanded', value: 'false', isSynced: false }, - { name: 'promotedAttributesExpanded', value: 'false', isSynced: true } + { name: 'promotedAttributesExpanded', value: 'true', isSynced: true }, + { name: 'similarNotesExpanded', value: 'true', isSynced: true } ]; function initStartupOptions() { diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js index 23a51c82e..1a2263703 100644 --- a/src/services/search/services/search.js +++ b/src/services/search/services/search.js @@ -102,7 +102,7 @@ function searchNotesForAutocomplete(query) { fuzzyAttributeSearch: true }); - const results = searchTrimmedNotes(query, parsingContext); + const {results} = searchTrimmedNotes(query, parsingContext); highlightSearchResults(results, parsingContext.highlightedTokens);