mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	redesign of search input. Saved search is now saved under active note and doesn't need page reload
This commit is contained in:
		| @@ -123,7 +123,10 @@ if (utils.isElectron()) { | |||||||
|         setTimeout(async () => { |         setTimeout(async () => { | ||||||
|             const parentNode = treeService.getActiveNode(); |             const parentNode = treeService.getActiveNode(); | ||||||
|  |  | ||||||
|             const {note} = await treeService.createNote(parentNode, parentNode.data.noteId, 'into', "text", parentNode.data.isProtected); |             const {note} = await treeService.createNote(parentNode, parentNode.data.noteId, 'into', { | ||||||
|  |                 type: "text", | ||||||
|  |                 isProtected: parentNode.data.isProtected | ||||||
|  |             }); | ||||||
|  |  | ||||||
|             await treeService.activateNote(note.noteId); |             await treeService.activateNote(note.noteId); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -91,10 +91,10 @@ $("#note-menu-button").click(async e => { | |||||||
|             const parentNoteId = node.data.parentNoteId; |             const parentNoteId = node.data.parentNoteId; | ||||||
|             const isProtected = treeUtils.getParentProtectedStatus(node); |             const isProtected = treeUtils.getParentProtectedStatus(node); | ||||||
|  |  | ||||||
|             treeService.createNote(node, parentNoteId, 'after', null, isProtected); |             treeService.createNote(node, parentNoteId, 'after', { isProtected: isProtected }); | ||||||
|         } |         } | ||||||
|         else if (cmd === "insertChildNote") { |         else if (cmd === "insertChildNote") { | ||||||
|             treeService.createNote(node, node.data.noteId, 'into', null); |             treeService.createNote(node, node.data.noteId, 'into'); | ||||||
|         } |         } | ||||||
|         else if (cmd === "delete") { |         else if (cmd === "delete") { | ||||||
|             treeChangesService.deleteNodes([node]); |             treeChangesService.deleteNodes([node]); | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import treeService from './tree.js'; | import treeService from './tree.js'; | ||||||
|  | import treeCache from "./tree_cache.js"; | ||||||
| import server from './server.js'; | import server from './server.js'; | ||||||
| import treeUtils from "./tree_utils.js"; | import infoService from "./info.js"; | ||||||
|  |  | ||||||
| const $tree = $("#tree"); | const $tree = $("#tree"); | ||||||
| const $searchInput = $("input[name='search-text']"); | const $searchInput = $("input[name='search-text']"); | ||||||
| @@ -64,16 +65,35 @@ async function doSearch(searchText) { | |||||||
|  |  | ||||||
|         $searchResultsInner.append($result); |         $searchResultsInner.append($result); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // have at least some feedback which is good especially in situations | ||||||
|  |     // when the result list does not change with a query | ||||||
|  |     infoService.showMessage("Search finished successfully."); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function saveSearch() { | async function saveSearch() { | ||||||
|     const {noteId} = await server.post('search/' + encodeURIComponent($searchInput.val())); |     const searchString = $searchInput.val().trim(); | ||||||
|  |  | ||||||
|  |     if (searchString.length === 0) { | ||||||
|  |         alert("Write some search criteria first so there is something to save."); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let activeNode = treeService.getActiveNode(); | ||||||
|  |     const parentNote = await treeCache.getNote(activeNode.data.noteId); | ||||||
|  |  | ||||||
|  |     if (parentNote.type === 'search') { | ||||||
|  |         activeNode = activeNode.getParent(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await treeService.createNote(activeNode, activeNode.data.noteId, 'into', { | ||||||
|  |         type: "search", | ||||||
|  |         mime: "application/json", | ||||||
|  |         title: searchString, | ||||||
|  |         content: JSON.stringify({ searchString: searchString }) | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     resetSearch(); |     resetSearch(); | ||||||
|  |  | ||||||
|     await treeService.reload(); |  | ||||||
|  |  | ||||||
|     await treeService.activateNote(noteId); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function init() { | function init() { | ||||||
|   | |||||||
| @@ -560,49 +560,44 @@ async function createNewTopLevelNote() { | |||||||
|  |  | ||||||
|     const rootNode = getNodesByNoteId(hoistedNoteId)[0]; |     const rootNode = getNodesByNoteId(hoistedNoteId)[0]; | ||||||
|  |  | ||||||
|     await createNote(rootNode, hoistedNoteId, "into", null, false); |     await createNote(rootNode, hoistedNoteId, "into"); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | async function createNote(node, parentNoteId, target, extraOptions) { | ||||||
|  * @param type - type can be falsy - in that case it will be chosen automatically based on parent note |  | ||||||
|  */ |  | ||||||
| async function createNote(node, parentNoteId, target, type, isProtected, saveSelection = false) { |  | ||||||
|     utils.assertArguments(node, parentNoteId, target); |     utils.assertArguments(node, parentNoteId, target); | ||||||
|  |  | ||||||
|     // if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted |     // if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted | ||||||
|     // but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often |     // but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often | ||||||
|     if (!isProtected || !protectedSessionHolder.isProtectedSessionAvailable()) { |     if (!extraOptions.isProtected || !protectedSessionHolder.isProtectedSessionAvailable()) { | ||||||
|         isProtected = false; |         extraOptions.isProtected = false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (noteDetailService.getActiveNoteType() !== 'text') { |     if (noteDetailService.getActiveNoteType() !== 'text') { | ||||||
|         saveSelection = false; |         extraOptions.saveSelection = false; | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         // just disable this feature altogether - there's a problem that note containing image or table at the beginning |         // just disable this feature altogether - there's a problem that note containing image or table at the beginning | ||||||
|         // of the content will be auto-selected by CKEditor and then CTRL-P with no user interaction will automatically save |         // of the content will be auto-selected by CKEditor and then CTRL-P with no user interaction will automatically save | ||||||
|         // the selection - see https://github.com/ckeditor/ckeditor5/issues/1384 |         // the selection - see https://github.com/ckeditor/ckeditor5/issues/1384 | ||||||
|         saveSelection = false; |         extraOptions.saveSelection = false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let title, content; |     if (extraOptions.saveSelection) { | ||||||
|  |         [extraOptions.title, extraOptions.content] = parseSelectedHtml(window.cutToNote.getSelectedHtml()); | ||||||
|     if (saveSelection) { |  | ||||||
|         [title, content] = parseSelectedHtml(window.cutToNote.getSelectedHtml()); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const newNoteName = title || "new note"; |     const newNoteName = extraOptions.title || "new note"; | ||||||
|  |  | ||||||
|     const {note, branch} = await server.post('notes/' + parentNoteId + '/children', { |     const {note, branch} = await server.post('notes/' + parentNoteId + '/children', { | ||||||
|         title: newNoteName, |         title: newNoteName, | ||||||
|         content: content, |         content: extraOptions.content, | ||||||
|         target: target, |         target: target, | ||||||
|         target_branchId: node.data.branchId, |         target_branchId: node.data.branchId, | ||||||
|         isProtected: isProtected, |         isProtected: extraOptions.isProtected, | ||||||
|         type: type |         type: extraOptions.type | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (saveSelection) { |     if (extraOptions.saveSelection) { | ||||||
|         // we remove the selection only after it was saved to server to make sure we don't lose anything |         // we remove the selection only after it was saved to server to make sure we don't lose anything | ||||||
|         window.cutToNote.removeSelection(); |         window.cutToNote.removeSelection(); | ||||||
|     } |     } | ||||||
| @@ -622,9 +617,11 @@ async function createNote(node, parentNoteId, target, type, isProtected, saveSel | |||||||
|         parentNoteId: parentNoteId, |         parentNoteId: parentNoteId, | ||||||
|         refKey: branchEntity.noteId, |         refKey: branchEntity.noteId, | ||||||
|         branchId: branchEntity.branchId, |         branchId: branchEntity.branchId, | ||||||
|         isProtected: isProtected, |         isProtected: extraOptions.isProtected, | ||||||
|         extraClasses: await treeBuilder.getExtraClasses(noteEntity), |         extraClasses: await treeBuilder.getExtraClasses(noteEntity), | ||||||
|         icon: await treeBuilder.getIcon(noteEntity) |         icon: await treeBuilder.getIcon(noteEntity), | ||||||
|  |         folder: extraOptions.type === 'search', | ||||||
|  |         lazy: true | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     if (target === 'after') { |     if (target === 'after') { | ||||||
| @@ -708,13 +705,19 @@ utils.bindShortcut('ctrl+o', async () => { | |||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     createNote(node, parentNoteId, 'after', null, isProtected, true); |     await createNote(node, parentNoteId, 'after', { | ||||||
|  |         isProtected: isProtected, | ||||||
|  |         saveSelection: true | ||||||
|  |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| function createNoteInto() { | async function createNoteInto() { | ||||||
|     const node = getActiveNode(); |     const node = getActiveNode(); | ||||||
|  |  | ||||||
|     createNote(node, node.data.noteId, 'into', null, node.data.isProtected, true); |     await createNote(node, node.data.noteId, 'into', { | ||||||
|  |         isProtected: node.data.isProtected, | ||||||
|  |         saveSelection: true | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function checkFolderStatus(node) { | async function checkFolderStatus(node) { | ||||||
| @@ -768,10 +771,8 @@ $scrollToActiveNoteButton.click(scrollToActiveNote); | |||||||
| export default { | export default { | ||||||
|     reload, |     reload, | ||||||
|     collapseTree, |     collapseTree, | ||||||
|     scrollToActiveNote, |  | ||||||
|     setBranchBackgroundBasedOnProtectedStatus, |     setBranchBackgroundBasedOnProtectedStatus, | ||||||
|     setProtected, |     setProtected, | ||||||
|     expandToNote, |  | ||||||
|     activateNote, |     activateNote, | ||||||
|     getFocusedNode, |     getFocusedNode, | ||||||
|     getActiveNode, |     getActiveNode, | ||||||
| @@ -779,7 +780,6 @@ export default { | |||||||
|     setCurrentNotePathToHash, |     setCurrentNotePathToHash, | ||||||
|     setNoteTitle, |     setNoteTitle, | ||||||
|     setPrefix, |     setPrefix, | ||||||
|     createNewTopLevelNote, |  | ||||||
|     createNote, |     createNote, | ||||||
|     createNoteInto, |     createNoteInto, | ||||||
|     getSelectedNodes, |     getSelectedNodes, | ||||||
|   | |||||||
| @@ -165,12 +165,15 @@ function selectContextMenuItem(event, cmd) { | |||||||
|         const isProtected = treeUtils.getParentProtectedStatus(node); |         const isProtected = treeUtils.getParentProtectedStatus(node); | ||||||
|         const type = cmd.split("_")[1]; |         const type = cmd.split("_")[1]; | ||||||
|  |  | ||||||
|         treeService.createNote(node, parentNoteId, 'after', type, isProtected); |         treeService.createNote(node, parentNoteId, 'after', { | ||||||
|  |             type: type, | ||||||
|  |             isProtected: isProtected | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|     else if (cmd.startsWith("insertChildNote")) { |     else if (cmd.startsWith("insertChildNote")) { | ||||||
|         const type = cmd.split("_")[1]; |         const type = cmd.split("_")[1]; | ||||||
|  |  | ||||||
|         treeService.createNote(node, node.data.noteId, 'into', type); |         treeService.createNote(node, node.data.noteId, 'into', { type: type }); | ||||||
|     } |     } | ||||||
|     else if (cmd === "editBranchPrefix") { |     else if (cmd === "editBranchPrefix") { | ||||||
|         branchPrefixDialog.showDialog(node); |         branchPrefixDialog.showDialog(node); | ||||||
|   | |||||||
| @@ -66,7 +66,7 @@ body { | |||||||
|     display: flex; |     display: flex; | ||||||
|     justify-content: space-around; |     justify-content: space-around; | ||||||
|     padding: 10px 0 10px 0; |     padding: 10px 0 10px 0; | ||||||
|     margin: 0 20px 0 10px; |     margin: 0 10px 0 10px; | ||||||
|     border: 1px solid var(--main-border-color); |     border: 1px solid var(--main-border-color); | ||||||
|     border-radius: 7px; |     border-radius: 7px; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const noteService = require('../../services/notes'); |  | ||||||
| const repository = require('../../services/repository'); | const repository = require('../../services/repository'); | ||||||
| const noteCacheService = require('../../services/note_cache'); | const noteCacheService = require('../../services/note_cache'); | ||||||
| const log = require('../../services/log'); | const log = require('../../services/log'); | ||||||
| @@ -13,20 +12,6 @@ async function searchNotes(req) { | |||||||
|     return noteIds.map(noteCacheService.getNotePath).filter(res => !!res); |     return noteIds.map(noteCacheService.getNotePath).filter(res => !!res); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function saveSearchToNote(req) { |  | ||||||
|     const content = { |  | ||||||
|         searchString: req.params.searchString |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     const {note} = await noteService.createNote('root', req.params.searchString, content, { |  | ||||||
|         json: true, |  | ||||||
|         type: 'search', |  | ||||||
|         mime: "application/json" |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return { noteId: note.noteId }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function searchFromNote(req) { | async function searchFromNote(req) { | ||||||
|     const note = await repository.getNote(req.params.noteId); |     const note = await repository.getNote(req.params.noteId); | ||||||
|  |  | ||||||
| @@ -52,7 +37,7 @@ async function searchFromNote(req) { | |||||||
|         noteIds = await searchFromRelation(note, relationName); |         noteIds = await searchFromRelation(note, relationName); | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         noteIds = searchService.searchForNoteIds(json.searchString); |         noteIds = await searchService.searchForNoteIds(json.searchString); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // we won't return search note's own noteId |     // we won't return search note's own noteId | ||||||
| @@ -100,6 +85,5 @@ async function searchFromRelation(note, relationName) { | |||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     searchNotes, |     searchNotes, | ||||||
|     saveSearchToNote, |  | ||||||
|     searchFromNote |     searchFromNote | ||||||
| }; | }; | ||||||
| @@ -209,7 +209,6 @@ function register(app) { | |||||||
|     route(POST, '/api/sender/note', [auth.checkSenderToken], senderRoute.saveNote, apiResultHandler); |     route(POST, '/api/sender/note', [auth.checkSenderToken], senderRoute.saveNote, apiResultHandler); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes); |     apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes); | ||||||
|     apiRoute(POST, '/api/search/:searchString', searchRoute.saveSearchToNote); |  | ||||||
|     apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote); |     apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote); | ||||||
|  |  | ||||||
|     route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); |     route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); | ||||||
|   | |||||||
| @@ -1,6 +1,17 @@ | |||||||
| const utils = require('./utils'); | const utils = require('./utils'); | ||||||
|  |  | ||||||
| const VIRTUAL_ATTRIBUTES = ["dateCreated", "dateCreated", "dateModified", "utcDateCreated", "utcDateModified", "isProtected", "title", "content", "type", "mime", "text"]; | const VIRTUAL_ATTRIBUTES = [ | ||||||
|  |     "dateCreated", | ||||||
|  |     "dateModified", | ||||||
|  |     "utcDateCreated", | ||||||
|  |     "utcDateModified", | ||||||
|  |     "isProtected", | ||||||
|  |     "title", | ||||||
|  |     "content", | ||||||
|  |     "type", | ||||||
|  |     "mime", | ||||||
|  |     "text" | ||||||
|  | ]; | ||||||
|  |  | ||||||
| module.exports = function(filters, selectedColumns = 'notes.*') { | module.exports = function(filters, selectedColumns = 'notes.*') { | ||||||
|     // alias => join |     // alias => join | ||||||
| @@ -106,7 +117,7 @@ module.exports = function(filters, selectedColumns = 'notes.*') { | |||||||
|         else if (filter.operator === '=*' || filter.operator === '!=*') { |         else if (filter.operator === '=*' || filter.operator === '!=*') { | ||||||
|             where += `${accessor}` |             where += `${accessor}` | ||||||
|                     + (filter.operator.includes('!') ? ' NOT' : '') |                     + (filter.operator.includes('!') ? ' NOT' : '') | ||||||
|                     + ` LIKE '` + utils.prepareSqlForLike('', filter.value, '%'); |                     + ` LIKE ` + utils.prepareSqlForLike('', filter.value, '%'); | ||||||
|         } |         } | ||||||
|         else if (filter.operator === '*=*' || filter.operator === '!*=*') { |         else if (filter.operator === '*=*' || filter.operator === '!*=*') { | ||||||
|             where += `${accessor}` |             where += `${accessor}` | ||||||
| @@ -149,8 +160,5 @@ module.exports = function(filters, selectedColumns = 'notes.*') { | |||||||
|               GROUP BY notes.noteId |               GROUP BY notes.noteId | ||||||
|               ORDER BY ${orderBy.join(", ")}`; |               ORDER BY ${orderBy.join(", ")}`; | ||||||
|  |  | ||||||
|     console.log(query); |  | ||||||
|     console.log(params); |  | ||||||
|  |  | ||||||
|     return { query, params }; |     return { query, params }; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -77,6 +77,18 @@ async function createNewNote(parentNoteId, noteData) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (!noteData.mime) { | ||||||
|  |         if (noteData.type === 'text') { | ||||||
|  |             noteData.mime = 'text/html'; | ||||||
|  |         } | ||||||
|  |         else if (noteData.type === 'code') { | ||||||
|  |             noteData.mime = 'text/plain'; | ||||||
|  |         } | ||||||
|  |         else if (noteData.type === 'relation-map' || noteData.type === 'search') { | ||||||
|  |             noteData.mime = 'application/json'; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     noteData.type = noteData.type || parentNote.type; |     noteData.type = noteData.type || parentNote.type; | ||||||
|     noteData.mime = noteData.mime || parentNote.mime; |     noteData.mime = noteData.mime || parentNote.mime; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| const dayjs = require("dayjs"); | const dayjs = require("dayjs"); | ||||||
|  |  | ||||||
| const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\w_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([\w_-]+|"[^"]+"))?/ig; | const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\w_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([\w_/-]+|"[^"]+"))?/ig; | ||||||
| const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i; | const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i; | ||||||
|  |  | ||||||
| function calculateSmartValue(v) { | function calculateSmartValue(v) { | ||||||
|   | |||||||
| @@ -105,17 +105,23 @@ | |||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div id="search-box"> |         <div id="search-box"> | ||||||
|             <div style="display: flex; align-items: center; flex-wrap: wrap;"> |             <div class="form-group"> | ||||||
|                 <input name="search-text" id="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px; flex-basis: 5em; min-width: 0;" autocomplete="off"> |                 <div class="input-group"> | ||||||
|  |                     <input name="search-text" id="search-text" class="form-control" placeholder="Search text, labels" autocomplete="off"> | ||||||
|  |  | ||||||
|  |                     <div class="input-group-append"> | ||||||
|                         <button id="do-search-button" class="btn btn-sm icon-button jam jam-search" title="Search (enter)"></button> |                         <button id="do-search-button" class="btn btn-sm icon-button jam jam-search" title="Search (enter)"></button> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|                   |  | ||||||
|  |  | ||||||
|                 <button id="save-search-button" class="btn btn-sm icon-button jam jam-save" title="Save search"></button> |             <div style="display: flex; align-items: center; justify-content: space-evenly; flex-wrap: wrap;"> | ||||||
|  |                 <button id="save-search-button" class="btn btn-sm" | ||||||
|  |                     title="This will create new saved search note under active note."> | ||||||
|  |                     <span class="jam jam-save"></span> Save search</button> | ||||||
|  |  | ||||||
|                   |                 <button id="close-search-button" class="btn btn-sm"><span class="jam jam-close"></span> Close search</button> | ||||||
|  |  | ||||||
|                 <button id="close-search-button" class="btn btn-sm icon-button jam jam-close" title="Close search"></button> |  | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user