mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	added possibility to search by attached script returning note ids + some refactorings
This commit is contained in:
		| @@ -41,7 +41,7 @@ import macInit from './services/mac_init.js'; | ||||
| import cssLoader from './services/css_loader.js'; | ||||
|  | ||||
| // required for CKEditor image upload plugin | ||||
| window.glob.getCurrentNode = treeService.getCurrentNode; | ||||
| window.glob.getActiveNode = treeService.getActiveNode; | ||||
| window.glob.getHeaders = server.getHeaders; | ||||
| window.glob.showAddLinkDialog = addLinkDialog.showDialog; | ||||
| // this is required by CKEditor when uploading images | ||||
| @@ -120,7 +120,7 @@ if (utils.isElectron()) { | ||||
|         await treeService.activateNote(parentNoteId); | ||||
|  | ||||
|         setTimeout(async () => { | ||||
|             const parentNode = treeService.getCurrentNode(); | ||||
|             const parentNode = treeService.getActiveNode(); | ||||
|  | ||||
|             const {note} = await treeService.createNote(parentNode, parentNode.data.noteId, 'into', "text", parentNode.data.isProtected); | ||||
|  | ||||
|   | ||||
| @@ -16,7 +16,7 @@ async function showDialog() { | ||||
|  | ||||
|     $dialog.modal(); | ||||
|  | ||||
|     const currentNode = treeService.getCurrentNode(); | ||||
|     const currentNode = treeService.getActiveNode(); | ||||
|  | ||||
|     branchId = currentNode.data.branchId; | ||||
|     const branch = await treeCache.getBranch(branchId); | ||||
|   | ||||
| @@ -45,7 +45,7 @@ async function showDialog(defaultType) { | ||||
|  | ||||
|     $dialog.modal(); | ||||
|  | ||||
|     const currentNode = treeService.getCurrentNode(); | ||||
|     const currentNode = treeService.getActiveNode(); | ||||
|     const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId); | ||||
|  | ||||
|     $noteTitle.html(noteTitle); | ||||
| @@ -69,7 +69,7 @@ $form.submit(() => { | ||||
|  | ||||
|     const exportVersion = exportFormat === 'opml' ? $dialog.find("input[name='opml-version']:checked").val() : "1.0"; | ||||
|  | ||||
|     const currentNode = treeService.getCurrentNode(); | ||||
|     const currentNode = treeService.getActiveNode(); | ||||
|  | ||||
|     exportBranch(currentNode.data.branchId, exportType, exportFormat, exportVersion); | ||||
|  | ||||
|   | ||||
| @@ -35,14 +35,14 @@ async function showDialog() { | ||||
|  | ||||
|     glob.activeDialog = $dialog; | ||||
|  | ||||
|     const currentNode = treeService.getCurrentNode(); | ||||
|     const currentNode = treeService.getActiveNode(); | ||||
|     $noteTitle.text(await treeUtils.getNoteTitle(currentNode.data.noteId)); | ||||
|  | ||||
|     $dialog.modal(); | ||||
| } | ||||
|  | ||||
| $form.submit(() => { | ||||
|     const currentNode = treeService.getCurrentNode(); | ||||
|     const currentNode = treeService.getActiveNode(); | ||||
|  | ||||
|     // disabling so that import is not triggered again. | ||||
|     $importButton.attr("disabled", "disabled"); | ||||
|   | ||||
| @@ -71,7 +71,7 @@ async function showTree() { | ||||
| } | ||||
|  | ||||
| $("#note-menu-button").click(async e => { | ||||
|     const node = treeService.getCurrentNode(); | ||||
|     const node = treeService.getActiveNode(); | ||||
|     const branch = await treeCache.getBranch(node.data.branchId); | ||||
|     const note = await treeCache.getNote(node.data.noteId); | ||||
|     const parentNote = await treeCache.getNote(branch.parentNoteId); | ||||
|   | ||||
| @@ -138,7 +138,7 @@ function registerEntrypoints() { | ||||
|  | ||||
|     // FIXME: do we really need these at this point? | ||||
|     utils.bindShortcut("ctrl+shift+up", () => { | ||||
|         const node = treeService.getCurrentNode(); | ||||
|         const node = treeService.getActiveNode(); | ||||
|         node.navigate($.ui.keyCode.UP, true); | ||||
|  | ||||
|         $("#note-detail-text").focus(); | ||||
| @@ -147,7 +147,7 @@ function registerEntrypoints() { | ||||
|  | ||||
|     // FIXME: do we really need these at this point? | ||||
|     utils.bindShortcut("ctrl+shift+down", () => { | ||||
|         const node = treeService.getCurrentNode(); | ||||
|         const node = treeService.getActiveNode(); | ||||
|         node.navigate($.ui.keyCode.DOWN, true); | ||||
|  | ||||
|         $("#note-detail-text").focus(); | ||||
|   | ||||
| @@ -187,7 +187,7 @@ async function loadNoteDetail(noteId) { | ||||
|     // this is useful when user quickly switches notes (by e.g. holding down arrow) so that we don't | ||||
|     // try to render all those loaded notes one after each other. This only guarantees that correct note | ||||
|     // will be displayed independent of timing | ||||
|     const currentTreeNode = treeService.getCurrentNode(); | ||||
|     const currentTreeNode = treeService.getActiveNode(); | ||||
|     if (currentTreeNode && currentTreeNode.data.noteId !== loadedNote.noteId) { | ||||
|         return; | ||||
|     } | ||||
| @@ -196,7 +196,7 @@ async function loadNoteDetail(noteId) { | ||||
|     activeNote = loadedNote; | ||||
|  | ||||
|     if (utils.isDesktop()) { | ||||
|         // needs to happen after loading the note itself because it references current noteId | ||||
|         // needs to happen after loading the note itself because it references active noteId | ||||
|         attributeService.refreshAttributes(); | ||||
|     } | ||||
|     else { | ||||
|   | ||||
| @@ -43,7 +43,7 @@ function ensureProtectedSession(requireProtectedSession, modal) { | ||||
|         $noteTitle.prop("readonly", true); | ||||
|  | ||||
|         if (modal) { | ||||
|             if (treeService.getCurrentNode().data.isProtected) { | ||||
|             if (treeService.getActiveNode().data.isProtected) { | ||||
|                 $noteDetailWrapper.hide(); | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -20,7 +20,7 @@ import confirmDialog from "../dialogs/confirm.js"; | ||||
| const $tree = $("#tree"); | ||||
| const $createTopLevelNoteButton = $("#create-top-level-note-button"); | ||||
| const $collapseTreeButton = $("#collapse-tree-button"); | ||||
| const $scrollToCurrentNoteButton = $("#scroll-to-current-note-button"); | ||||
| const $scrollToActiveNoteButton = $("#scroll-to-active-note-button"); | ||||
| const $notePathList = $("#note-path-list"); | ||||
| const $notePathCount = $("#note-path-count"); | ||||
|  | ||||
| @@ -35,12 +35,12 @@ function getFocusedNode() { | ||||
| } | ||||
|  | ||||
| // note that if you want to access data like noteId or isProtected, you need to go into "data" property | ||||
| function getCurrentNode() { | ||||
| function getActiveNode() { | ||||
|     return $tree.fancytree("getActiveNode"); | ||||
| } | ||||
|  | ||||
| function getActiveNotePath() { | ||||
|     const node = getCurrentNode(); | ||||
|     const node = getActiveNode(); | ||||
|  | ||||
|     return treeUtils.getNotePath(node); | ||||
| } | ||||
| @@ -356,7 +356,7 @@ function clearSelectedNodes() { | ||||
|         selectedNode.setSelected(false); | ||||
|     } | ||||
|  | ||||
|     const currentNode = getCurrentNode(); | ||||
|     const currentNode = getActiveNode(); | ||||
|  | ||||
|     if (currentNode) { | ||||
|         currentNode.setSelected(true); | ||||
| @@ -520,8 +520,8 @@ async function collapseTree(node = null) { | ||||
|     node.visit(node => node.setExpanded(false)); | ||||
| } | ||||
|  | ||||
| function scrollToCurrentNote() { | ||||
|     const node = getCurrentNode(); | ||||
| function scrollToActiveNote() { | ||||
|     const node = getActiveNode(); | ||||
|  | ||||
|     if (node) { | ||||
|         node.makeVisible({scrollIntoView: true}); | ||||
| @@ -697,7 +697,7 @@ messagingService.subscribeToSyncMessages(syncData => { | ||||
| }); | ||||
|  | ||||
| utils.bindShortcut('ctrl+o', async () => { | ||||
|     const node = getCurrentNode(); | ||||
|     const node = getActiveNode(); | ||||
|     const parentNoteId = node.data.parentNoteId; | ||||
|     const isProtected = treeUtils.getParentProtectedStatus(node); | ||||
|  | ||||
| @@ -709,7 +709,7 @@ utils.bindShortcut('ctrl+o', async () => { | ||||
| }); | ||||
|  | ||||
| function createNoteInto() { | ||||
|     const node = getCurrentNode(); | ||||
|     const node = getActiveNode(); | ||||
|  | ||||
|     createNote(node, node.data.noteId, 'into', null, node.data.isProtected, true); | ||||
| } | ||||
| @@ -742,7 +742,7 @@ window.glob.createNoteInto = createNoteInto; | ||||
|  | ||||
| utils.bindShortcut('ctrl+p', createNoteInto); | ||||
|  | ||||
| utils.bindShortcut('ctrl+.', scrollToCurrentNote); | ||||
| utils.bindShortcut('ctrl+.', scrollToActiveNote); | ||||
|  | ||||
| $(window).bind('hashchange', function() { | ||||
|     if (isNotePathInAddress()) { | ||||
| @@ -760,18 +760,18 @@ utils.bindShortcut('alt+c', () => collapseTree()); // don't use shortened form s | ||||
| $collapseTreeButton.click(() => collapseTree()); | ||||
|  | ||||
| $createTopLevelNoteButton.click(createNewTopLevelNote); | ||||
| $scrollToCurrentNoteButton.click(scrollToCurrentNote); | ||||
| $scrollToActiveNoteButton.click(scrollToActiveNote); | ||||
|  | ||||
| export default { | ||||
|     reload, | ||||
|     collapseTree, | ||||
|     scrollToCurrentNote, | ||||
|     scrollToActiveNote, | ||||
|     setBranchBackgroundBasedOnProtectedStatus, | ||||
|     setProtected, | ||||
|     expandToNote, | ||||
|     activateNote, | ||||
|     getFocusedNode, | ||||
|     getCurrentNode, | ||||
|     getActiveNode, | ||||
|     getActiveNotePath, | ||||
|     setCurrentNotePathToHash, | ||||
|     setNoteTitle, | ||||
|   | ||||
| @@ -115,22 +115,7 @@ async function prepareRealBranch(parentNote) { | ||||
| } | ||||
|  | ||||
| async function prepareSearchBranch(note) { | ||||
|     const fullNote = await noteDetailService.loadNote(note.noteId); | ||||
|  | ||||
|     console.log("ZZZ", fullNote.noteContent.content); | ||||
|  | ||||
|     if (!fullNote.noteContent.content || !fullNote.noteContent.content.trim()) { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     const json = JSON.parse(fullNote.noteContent.content); | ||||
|  | ||||
|     if (!json.searchString || !json.searchString.trim()) { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     const results = (await server.get('search/' + encodeURIComponent(json.searchString))) | ||||
|         .filter(res => res.noteId !== note.noteId); // this is necessary because title of the search note is often the same as the search text which would match and create circle | ||||
|     const results = await server.get('search-note/' + note.noteId); | ||||
|  | ||||
|     // force to load all the notes at once instead of one by one | ||||
|     await treeCache.getNotes(results.map(res => res.noteId)); | ||||
| @@ -149,7 +134,7 @@ async function prepareSearchBranch(note) { | ||||
|         treeCache.addBranch(branch); | ||||
|     } | ||||
|  | ||||
|     return await prepareRealBranch(fullNote); | ||||
|     return await prepareRealBranch(note); | ||||
| } | ||||
|  | ||||
| async function getExtraClasses(note) { | ||||
|   | ||||
| @@ -158,7 +158,7 @@ async function getContextMenuItems(event) { | ||||
|  | ||||
| function selectContextMenuItem(event, cmd) { | ||||
|     // context menu is always triggered on current node | ||||
|     const node = treeService.getCurrentNode(); | ||||
|     const node = treeService.getActiveNode(); | ||||
|  | ||||
|     if (cmd.startsWith("insertNoteAfter")) { | ||||
|         const parentNoteId = node.data.parentNoteId; | ||||
|   | ||||
							
								
								
									
										2
									
								
								src/public/libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/public/libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -28,7 +28,7 @@ | ||||
|     } | ||||
|  | ||||
|     async function validatorJavaScript(text, options) { | ||||
|         if (glob.getCurrentNote().mime === 'application/json') { | ||||
|         if (glob.getActiveNote().mime === 'application/json') { | ||||
|             // eslint doesn't seem to validate pure JSON well | ||||
|             return []; | ||||
|         } | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const noteService = require('../../services/notes'); | ||||
| const repository = require('../../services/repository'); | ||||
| const noteCacheService = require('../../services/note_cache'); | ||||
| const log = require('../../services/log'); | ||||
| const scriptService = require('../../services/script'); | ||||
| const searchService = require('../../services/search'); | ||||
|  | ||||
| async function searchNotes(req) { | ||||
| @@ -24,7 +27,79 @@ async function saveSearchToNote(req) { | ||||
|     return { noteId: note.noteId }; | ||||
| } | ||||
|  | ||||
| async function searchFromNote(req) { | ||||
|     const note = await repository.getNote(req.params.noteId); | ||||
|  | ||||
|     if (!note) { | ||||
|         return [404, `Note ${req.params.noteId} has not been found.`]; | ||||
|     } | ||||
|  | ||||
|     if (note.type !== 'search') { | ||||
|         return [400, '`Note ${req.params.noteId} is not search note.`'] | ||||
|     } | ||||
|  | ||||
|     const json = await note.getJsonContent(); | ||||
|  | ||||
|     if (!json || !json.searchString) { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     let noteIds; | ||||
|  | ||||
|     if (json.searchString.startsWith('=')) { | ||||
|         const relationName = json.searchString.substr(1).trim(); | ||||
|  | ||||
|         noteIds = await searchFromRelation(note, relationName); | ||||
|     } | ||||
|     else { | ||||
|         noteIds = searchService.searchForNoteIds(json.searchString); | ||||
|     } | ||||
|  | ||||
|     // we won't return search note's own noteId | ||||
|     noteIds = noteIds.filter(noteId => noteId !== note.noteId); | ||||
|  | ||||
|     return noteIds.map(noteCacheService.getNotePath).filter(res => !!res); | ||||
| } | ||||
|  | ||||
| async function searchFromRelation(note, relationName) { | ||||
|     const scriptNote = await note.getRelationTarget(relationName); | ||||
|  | ||||
|     if (!scriptNote) { | ||||
|         log.info(`Search note's relation ${relationName} has not been found.`); | ||||
|  | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     if (!scriptNote.isJavaScript() || scriptNote.getScriptEnv() !== 'backend') { | ||||
|         log.info(`Note ${scriptNote.noteId} is not executable.`); | ||||
|  | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     if (!note.isContentAvailable) { | ||||
|         log.info(`Note ${scriptNote.noteId} is not available outside of protected session.`); | ||||
|  | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     const result = await scriptService.executeNote(scriptNote, { originEntity: note }); | ||||
|  | ||||
|     if (!Array.isArray(result)) { | ||||
|         log.info(`Result from ${scriptNote.noteId} is not an array.`); | ||||
|  | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     if (result.length === 0) { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     // we expect either array of noteIds (strings) or notes, in that case we extract noteIds ourselves | ||||
|     return typeof result[0] === 'string' ? result : result.map(item => item.noteId); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     searchNotes, | ||||
|     saveSearchToNote | ||||
|     saveSearchToNote, | ||||
|     searchFromNote | ||||
| }; | ||||
| @@ -202,6 +202,7 @@ function register(app) { | ||||
|  | ||||
|     apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes); | ||||
|     apiRoute(POST, '/api/search/:searchString', searchRoute.saveSearchToNote); | ||||
|     apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote); | ||||
|  | ||||
|     route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); | ||||
|     // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username) | ||||
|   | ||||
| @@ -7,12 +7,14 @@ const log = require('./log'); | ||||
|  | ||||
| async function executeNote(note, apiParams) { | ||||
|     if (!note.isJavaScript() || note.getScriptEnv() !== 'backend' || !note.isContentAvailable) { | ||||
|         log.info(`Cannot execute note ${note.noteId}`); | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const bundle = await getScriptBundle(note); | ||||
|  | ||||
|     await executeBundle(bundle, apiParams); | ||||
|     return await executeBundle(bundle, apiParams); | ||||
| } | ||||
|  | ||||
| async function executeNoteNoException(note, apiParams) { | ||||
|   | ||||
| @@ -94,7 +94,7 @@ | ||||
|  | ||||
|             <a id="collapse-tree-button" title="Collapse note tree. Shortcut ALT+C" class="icon-action jam jam-layers"></a> | ||||
|  | ||||
|             <a id="scroll-to-current-note-button" title="Scroll to current note. Shortcut CTRL+." class="icon-action jam jam-download"></a> | ||||
|             <a id="scroll-to-active-note-button" title="Scroll to active note. Shortcut CTRL+." class="icon-action jam jam-download"></a> | ||||
|  | ||||
|             <a id="toggle-search-button" title="Search in notes. Shortcut CTRL+S" class="icon-action jam jam-search"></a> | ||||
|         </div> | ||||
|   | ||||
| @@ -13,17 +13,17 @@ | ||||
|             <form id="add-link-form"> | ||||
|                 <div class="modal-body"> | ||||
|                     <div id="add-link-type-div" class="radio"> | ||||
|                         <label title="Add HTML link to the selected note at cursor in current note"> | ||||
|                         <label title="Add HTML link to the selected note at cursor in active note"> | ||||
|                             <input type="radio" name="add-link-type" value="html"/> | ||||
|                             add normal HTML link</label> | ||||
|  | ||||
|                         <label title="Add selected note as a child of current note"> | ||||
|                         <label title="Add selected note as a child of active note"> | ||||
|                             <input type="radio" name="add-link-type" value="selected-to-current"/> | ||||
|                             add selected note to current note</label> | ||||
|                             add selected note to active note</label> | ||||
|  | ||||
|                         <label title="Add current note as a child of the selected note"> | ||||
|                         <label title="Add active note as a child of the selected note"> | ||||
|                             <input type="radio" name="add-link-type" value="current-to-selected"/> | ||||
|                             add current note to selected note</label> | ||||
|                             add active note to selected note</label> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="form-group"> | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|                                     <li><kbd>LEFT/RIGHT</kbd> - collapse/expand node</li> | ||||
|                                     <li><kbd>ALT+LEFT/RIGHT</kbd> - go back / forwards in the history</li> | ||||
|                                     <li><kbd>CTRL+J</kbd> - show <a class="external" href="https://github.com/zadam/trilium/wiki/Note-navigation#jump-to-note">"Jump to" dialog</a></li> | ||||
|                                     <li><kbd>CTRL+.</kbd> - scroll to current note</li> | ||||
|                                     <li><kbd>CTRL+.</kbd> - scroll to active note</li> | ||||
|                                     <li><kbd>BACKSPACE</kbd> - jumps to parent note</li> | ||||
|                                     <li><kbd>ALT+C</kbd> - collapse whole note tree</li> | ||||
|                                     <li><kbd>ALT+-</kbd> (alt with minus sign) - collapse sub-tree</li> | ||||
| @@ -35,9 +35,9 @@ | ||||
|  | ||||
|                             <p class="card-text"> | ||||
|                                 <ul> | ||||
|                                     <li><kbd>CTRL+O</kbd> - creates new note after the current note</li> | ||||
|                                     <li><kbd>CTRL+P</kbd> - creates new sub-note into current note</li> | ||||
|                                     <li><kbd>F2</kbd> - edit <a class="external" href="https://github.com/zadam/trilium/wiki/Tree concepts#prefix">prefix</a> of current note clone</li> | ||||
|                                     <li><kbd>CTRL+O</kbd> - creates new note after the active note</li> | ||||
|                                     <li><kbd>CTRL+P</kbd> - creates new sub-note into active note</li> | ||||
|                                     <li><kbd>F2</kbd> - edit <a class="external" href="https://github.com/zadam/trilium/wiki/Tree concepts#prefix">prefix</a> of active note clone</li> | ||||
|                                 </ul> | ||||
|                             </p> | ||||
|                         </div> | ||||
| @@ -54,9 +54,9 @@ | ||||
|                                     <li><kbd>SHIFT+UP/DOWN</kbd> - multi-select note above/below</li> | ||||
|                                     <li><kbd>CTRL+A</kbd> - select all notes in the current level</li> | ||||
|                                     <li><kbd>CTRL+click</kbd> - select note</li> | ||||
|                                 <li><kbd>CTRL+C</kbd> - copies current note (or current selection) into clipboard (used for <a class="external" href="https://github.com/zadam/trilium/wiki/Cloning notes">cloning</a>)</li> | ||||
|                                 <li><kbd>CTRL+C</kbd> - copies active note (or current selection) into clipboard (used for <a class="external" href="https://github.com/zadam/trilium/wiki/Cloning notes">cloning</a>)</li> | ||||
|                                     <li><kbd>CTRL+X</kbd> - cuts current (or current selection) note into clipboard (used for moving notes)</li> | ||||
|                                     <li><kbd>CTRL+V</kbd> - pastes note(s) as sub-note into current note (which is either move or clone depending on whether it was copied or cut into clipboard)</li> | ||||
|                                     <li><kbd>CTRL+V</kbd> - pastes note(s) as sub-note into active note (which is either move or clone depending on whether it was copied or cut into clipboard)</li> | ||||
|                                     <li><kbd>DEL</kbd> - delete note / sub-tree</li> | ||||
|                                 </ul> | ||||
|                             </p> | ||||
| @@ -73,7 +73,7 @@ | ||||
|                                     <li><kbd>CTRL+K</kbd> - create / edit external link</li> | ||||
|                                     <li><kbd>CTRL+L</kbd> - create internal link</li> | ||||
|                                     <li><kbd>ALT+T</kbd> - inserts current date and time at caret position</li> | ||||
|                                     <li><kbd>CTRL+.</kbd> - jump away to the tree pane and scroll to current note</li> | ||||
|                                     <li><kbd>CTRL+.</kbd> - jump away to the tree pane and scroll to active note</li> | ||||
|                                 </ul> | ||||
|                             </p> | ||||
|                         </div> | ||||
|   | ||||
| @@ -15,7 +15,7 @@ | ||||
|  | ||||
|             <a id="collapse-tree-button" title="Collapse note tree. Shortcut ALT+C" class="icon-action jam jam-layers"></a> | ||||
|  | ||||
|             <a id="scroll-to-current-note-button" title="Scroll to current note. Shortcut CTRL+." class="icon-action jam jam-download"></a> | ||||
|             <a id="scroll-to-active-note-button" title="Scroll to active note. Shortcut CTRL+." class="icon-action jam jam-download"></a> | ||||
|  | ||||
|             <div class="dropdown"> | ||||
|                 <a id="global-actions-button" title="Global actions" class="icon-action jam jam-cogs dropdown-toggle" data-toggle="dropdown"></a> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user