mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Compare commits
	
		
			14 Commits
		
	
	
		
			v0.5.4-bet
			...
			v0.5.6
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 85d32c66f2 | ||
|  | 214d2e7659 | ||
|  | f380bb7f65 | ||
|  | 0a9a032daa | ||
|  | 23a2b58b24 | ||
|  | aee64b2522 | ||
|  | 02e07ec03a | ||
|  | 3d2dc8e699 | ||
|  | c84e15c9be | ||
|  | e18d0b9fd4 | ||
|  | 52817504d1 | ||
|  | a3b31fab54 | ||
|  | bc4aa3e40a | ||
|  | 873ea67e9c | 
| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "description": "Trilium Notes", |   "description": "Trilium Notes", | ||||||
|   "version": "0.5.4-beta", |   "version": "0.5.6", | ||||||
|   "license": "AGPL-3.0-only", |   "license": "AGPL-3.0-only", | ||||||
|   "main": "electron.js", |   "main": "electron.js", | ||||||
|   "repository": { |   "repository": { | ||||||
|   | |||||||
| @@ -2,7 +2,9 @@ | |||||||
|  |  | ||||||
| const attributesDialog = (function() { | const attributesDialog = (function() { | ||||||
|     const dialogEl = $("#attributes-dialog"); |     const dialogEl = $("#attributes-dialog"); | ||||||
|  |     const saveAttributesButton = $("#save-attributes-button"); | ||||||
|     const attributesModel = new AttributesModel(); |     const attributesModel = new AttributesModel(); | ||||||
|  |     let attributeNames = []; | ||||||
|  |  | ||||||
|     function AttributesModel() { |     function AttributesModel() { | ||||||
|         const self = this; |         const self = this; | ||||||
| @@ -14,38 +16,112 @@ const attributesDialog = (function() { | |||||||
|  |  | ||||||
|             const attributes = await server.get('notes/' + noteId + '/attributes'); |             const attributes = await server.get('notes/' + noteId + '/attributes'); | ||||||
|  |  | ||||||
|             this.attributes(attributes); |             self.attributes(attributes.map(ko.observable)); | ||||||
|  |  | ||||||
|  |             addLastEmptyRow(); | ||||||
|  |  | ||||||
|  |             attributeNames = await server.get('attributes/names'); | ||||||
|  |  | ||||||
|  |             // attribute might not be rendered immediatelly so could not focus | ||||||
|  |             setTimeout(() => $(".attribute-name:last").focus(), 100); | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         this.addNewRow = function() { |         function isValid() { | ||||||
|             self.attributes.push({ |             for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) { | ||||||
|                 attributeId: '', |                 if (self.isEmptyName(i) || self.isNotUnique(i)) { | ||||||
|                 name: '', |                     return false; | ||||||
|                 value: '' |                 } | ||||||
|             }); |             } | ||||||
|         }; |  | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         this.save = async function() { |         this.save = async function() { | ||||||
|  |             // we need to defocus from input (in case of enter-triggered save) because value is updated | ||||||
|  |             // on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would | ||||||
|  |             // stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel. | ||||||
|  |             saveAttributesButton.focus(); | ||||||
|  |  | ||||||
|  |             if (!isValid()) { | ||||||
|  |                 alert("Please fix all validation errors and try saving again."); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             const noteId = noteEditor.getCurrentNoteId(); |             const noteId = noteEditor.getCurrentNoteId(); | ||||||
|  |  | ||||||
|             const attributes = await server.put('notes/' + noteId + '/attributes', this.attributes()); |             const attributesToSave = self.attributes() | ||||||
|  |                 .map(attr => attr()) | ||||||
|  |                 .filter(attr => attr.attributeId !== "" || attr.name !== ""); | ||||||
|  |  | ||||||
|             self.attributes(attributes); |             const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave); | ||||||
|  |  | ||||||
|  |             self.attributes(attributes.map(ko.observable)); | ||||||
|  |  | ||||||
|  |             addLastEmptyRow(); | ||||||
|  |  | ||||||
|             showMessage("Attributes have been saved."); |             showMessage("Attributes have been saved."); | ||||||
|  |  | ||||||
|  |             noteEditor.loadAttributeList(); | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  |         function addLastEmptyRow() { | ||||||
|  |             const attrs = self.attributes(); | ||||||
|  |             const last = attrs.length === 0 ? null : attrs[attrs.length - 1](); | ||||||
|  |  | ||||||
|  |             if (!last || last.name.trim() !== "" || last.value !== "") { | ||||||
|  |                 self.attributes.push(ko.observable({ | ||||||
|  |                     attributeId: '', | ||||||
|  |                     name: '', | ||||||
|  |                     value: '' | ||||||
|  |                 })); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.attributeChanged = function (row) { | ||||||
|  |             addLastEmptyRow(); | ||||||
|  |  | ||||||
|  |             for (const attr of self.attributes()) { | ||||||
|  |                 if (row.attributeId === attr().attributeId) { | ||||||
|  |                     attr.valueHasMutated(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         this.isNotUnique = function(index) { | ||||||
|  |             const cur = self.attributes()[index](); | ||||||
|  |  | ||||||
|  |             if (cur.name.trim() === "") { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) { | ||||||
|  |                 const attr = attrs[i](); | ||||||
|  |  | ||||||
|  |                 if (index !== i && cur.name === attr.name) { | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return false; | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         this.isEmptyName = function(index) { | ||||||
|  |             const cur = self.attributes()[index](); | ||||||
|  |  | ||||||
|  |             return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== ""); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function showDialog() { |     async function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = dialogEl; | ||||||
|  |  | ||||||
|  |         await attributesModel.loadAttributes(); | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         dialogEl.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: 800, |             width: 800, | ||||||
|             height: 500 |             height: 500 | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         attributesModel.loadAttributes(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'alt+a', e => { |     $(document).bind('keydown', 'alt+a', e => { | ||||||
| @@ -56,6 +132,54 @@ const attributesDialog = (function() { | |||||||
|  |  | ||||||
|     ko.applyBindings(attributesModel, document.getElementById('attributes-dialog')); |     ko.applyBindings(attributesModel, document.getElementById('attributes-dialog')); | ||||||
|  |  | ||||||
|  |     $(document).on('focus', '.attribute-name', function (e) { | ||||||
|  |         if (!$(this).hasClass("ui-autocomplete-input")) { | ||||||
|  |             $(this).autocomplete({ | ||||||
|  |                 // shouldn't be required and autocomplete should just accept array of strings, but that fails | ||||||
|  |                 // because we have overriden filter() function in init.js | ||||||
|  |                 source: attributeNames.map(attr => { | ||||||
|  |                     return { | ||||||
|  |                         label: attr, | ||||||
|  |                         value: attr | ||||||
|  |                     } | ||||||
|  |                 }), | ||||||
|  |                 minLength: 0 | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $(this).autocomplete("search", $(this).val()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     $(document).on('focus', '.attribute-value', async function (e) { | ||||||
|  |         if (!$(this).hasClass("ui-autocomplete-input")) { | ||||||
|  |             const attributeName = $(this).parent().parent().find('.attribute-name').val(); | ||||||
|  |  | ||||||
|  |             if (attributeName.trim() === "") { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName)); | ||||||
|  |  | ||||||
|  |             if (attributeValues.length === 0) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             $(this).autocomplete({ | ||||||
|  |                 // shouldn't be required and autocomplete should just accept array of strings, but that fails | ||||||
|  |                 // because we have overriden filter() function in init.js | ||||||
|  |                 source: attributeValues.map(attr => { | ||||||
|  |                     return { | ||||||
|  |                         label: attr, | ||||||
|  |                         value: attr | ||||||
|  |                     } | ||||||
|  |                 }), | ||||||
|  |                 minLength: 0 | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $(this).autocomplete("search", $(this).val()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|         showDialog |         showDialog | ||||||
|     }; |     }; | ||||||
|   | |||||||
| @@ -54,24 +54,6 @@ $(document).bind('keydown', 'ctrl+f', () => { | |||||||
|     } |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| $(document).bind('keydown', "ctrl+shift+left", () => { |  | ||||||
|     const node = noteTree.getCurrentNode(); |  | ||||||
|     node.navigate($.ui.keyCode.LEFT, true); |  | ||||||
|  |  | ||||||
|     $("#note-detail").focus(); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', "ctrl+shift+right", () => { |  | ||||||
|     const node = noteTree.getCurrentNode(); |  | ||||||
|     node.navigate($.ui.keyCode.RIGHT, true); |  | ||||||
|  |  | ||||||
|     $("#note-detail").focus(); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', "ctrl+shift+up", () => { | $(document).bind('keydown', "ctrl+shift+up", () => { | ||||||
|     const node = noteTree.getCurrentNode(); |     const node = noteTree.getCurrentNode(); | ||||||
|     node.navigate($.ui.keyCode.UP, true); |     node.navigate($.ui.keyCode.UP, true); | ||||||
| @@ -123,7 +105,7 @@ $(window).on('beforeunload', () => { | |||||||
| // Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words | // Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words | ||||||
| $.ui.autocomplete.filter = (array, terms) => { | $.ui.autocomplete.filter = (array, terms) => { | ||||||
|     if (!terms) { |     if (!terms) { | ||||||
|         return []; |         return array; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const startDate = new Date(); |     const startDate = new Date(); | ||||||
|   | |||||||
| @@ -9,6 +9,8 @@ const noteEditor = (function() { | |||||||
|     const unprotectButton = $("#unprotect-button"); |     const unprotectButton = $("#unprotect-button"); | ||||||
|     const noteDetailWrapperEl = $("#note-detail-wrapper"); |     const noteDetailWrapperEl = $("#note-detail-wrapper"); | ||||||
|     const noteIdDisplayEl = $("#note-id-display"); |     const noteIdDisplayEl = $("#note-id-display"); | ||||||
|  |     const attributeListEl = $("#attribute-list"); | ||||||
|  |     const attributeListInnerEl = $("#attribute-list-inner"); | ||||||
|  |  | ||||||
|     let editor = null; |     let editor = null; | ||||||
|     let codeEditor = null; |     let codeEditor = null; | ||||||
| @@ -187,6 +189,27 @@ const noteEditor = (function() { | |||||||
|  |  | ||||||
|         // after loading new note make sure editor is scrolled to the top |         // after loading new note make sure editor is scrolled to the top | ||||||
|         noteDetailWrapperEl.scrollTop(0); |         noteDetailWrapperEl.scrollTop(0); | ||||||
|  |  | ||||||
|  |         loadAttributeList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function loadAttributeList() { | ||||||
|  |         const noteId = getCurrentNoteId(); | ||||||
|  |  | ||||||
|  |         const attributes = await server.get('notes/' + noteId + '/attributes'); | ||||||
|  |  | ||||||
|  |         attributeListInnerEl.html(''); | ||||||
|  |  | ||||||
|  |         if (attributes.length > 0) { | ||||||
|  |             for (const attr of attributes) { | ||||||
|  |                 attributeListInnerEl.append(formatAttribute(attr) + " "); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             attributeListEl.show(); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             attributeListEl.hide(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function loadNote(noteId) { |     async function loadNote(noteId) { | ||||||
| @@ -290,6 +313,7 @@ const noteEditor = (function() { | |||||||
|         newNoteCreated, |         newNoteCreated, | ||||||
|         getEditor, |         getEditor, | ||||||
|         focus, |         focus, | ||||||
|         executeCurrentNote |         executeCurrentNote, | ||||||
|  |         loadAttributeList | ||||||
|     }; |     }; | ||||||
| })(); | })(); | ||||||
| @@ -3,7 +3,7 @@ | |||||||
| const noteTree = (function() { | const noteTree = (function() { | ||||||
|     const treeEl = $("#tree"); |     const treeEl = $("#tree"); | ||||||
|     const parentListEl = $("#parent-list"); |     const parentListEl = $("#parent-list"); | ||||||
|     const parentListListEl = $("#parent-list-list"); |     const parentListListEl = $("#parent-list-inner"); | ||||||
|  |  | ||||||
|     let startNotePath = null; |     let startNotePath = null; | ||||||
|     let notesTreeMap = {}; |     let notesTreeMap = {}; | ||||||
|   | |||||||
| @@ -119,3 +119,17 @@ function executeScript(script) { | |||||||
|     // last \r\n is necessary if script contains line comment on its last line |     // last \r\n is necessary if script contains line comment on its last line | ||||||
|     eval("(async function() {" + script + "\r\n})()"); |     eval("(async function() {" + script + "\r\n})()"); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function formatValueWithWhitespace(val) { | ||||||
|  |     return /[^\w_-]/.test(val) ? '"' + val + '"' : val; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function formatAttribute(attr) { | ||||||
|  |     let str = "@" + formatValueWithWhitespace(attr.name); | ||||||
|  |  | ||||||
|  |     if (attr.value !== "") { | ||||||
|  |         str += "=" + formatValueWithWhitespace(attr.value); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return str; | ||||||
|  | } | ||||||
| @@ -5,12 +5,18 @@ | |||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-areas: "header header" |     grid-template-areas: "header header" | ||||||
|                          "tree-actions title" |                          "tree-actions title" | ||||||
|  |                          "search note-content" | ||||||
|                          "tree note-content" |                          "tree note-content" | ||||||
|                          "parent-list note-content"; |                          "parent-list note-content" | ||||||
|  |                          "parent-list attribute-list"; | ||||||
|     grid-template-columns: 2fr 5fr; |     grid-template-columns: 2fr 5fr; | ||||||
|     grid-template-rows: auto |     grid-template-rows: auto | ||||||
|                         auto |                         auto | ||||||
|                         1fr; |                         auto | ||||||
|  |                         1fr | ||||||
|  |                         auto | ||||||
|  |                         auto; | ||||||
|  |  | ||||||
|     justify-content: center; |     justify-content: center; | ||||||
|     grid-gap: 10px; |     grid-gap: 10px; | ||||||
| } | } | ||||||
| @@ -134,6 +140,7 @@ div.ui-tooltip { | |||||||
|     margin-left: 20px; |     margin-left: 20px; | ||||||
|     border-top: 2px solid #eee; |     border-top: 2px solid #eee; | ||||||
|     padding-top: 10px; |     padding-top: 10px; | ||||||
|  |     grid-area: parent-list; | ||||||
| } | } | ||||||
|  |  | ||||||
| #parent-list ul { | #parent-list ul { | ||||||
| @@ -238,7 +245,7 @@ div.ui-tooltip { | |||||||
| #note-id-display { | #note-id-display { | ||||||
|     position: absolute; |     position: absolute; | ||||||
|     right: 10px; |     right: 10px; | ||||||
|     bottom: 5px; |     bottom: 8px; | ||||||
|     z-index: 1000; |     z-index: 1000; | ||||||
|     color: lightgrey; |     color: lightgrey; | ||||||
| } | } | ||||||
| @@ -250,3 +257,15 @@ div.ui-tooltip { | |||||||
| } | } | ||||||
|  |  | ||||||
| .cm-matchhighlight {background-color: #eeeeee} | .cm-matchhighlight {background-color: #eeeeee} | ||||||
|  |  | ||||||
|  | #attribute-list { | ||||||
|  |     grid-area: attribute-list; | ||||||
|  |     color: #777777; | ||||||
|  |     border-top: 1px solid #eee; | ||||||
|  |     padding: 5px; display: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #attribute-list button { | ||||||
|  |     padding: 2px; | ||||||
|  |     margin-right: 5px; | ||||||
|  | } | ||||||
| @@ -7,14 +7,15 @@ const auth = require('../../services/auth'); | |||||||
| const sync_table = require('../../services/sync_table'); | const sync_table = require('../../services/sync_table'); | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
|  | const attributes = require('../../services/attributes'); | ||||||
|  |  | ||||||
| router.get('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | router.get('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|  |  | ||||||
|     res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId])); |     res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId])); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|     const attributes = req.body; |     const attributes = req.body; | ||||||
|     const now = utils.nowDate(); |     const now = utils.nowDate(); | ||||||
| @@ -45,4 +46,26 @@ router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) | |||||||
|     res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId])); |     res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId])); | ||||||
| })); | })); | ||||||
|  |  | ||||||
|  | router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|  |     const names = await sql.getColumn("SELECT DISTINCT name FROM attributes"); | ||||||
|  |  | ||||||
|  |     for (const attr of attributes.BUILTIN_ATTRIBUTES) { | ||||||
|  |         if (!names.includes(attr)) { | ||||||
|  |             names.push(attr); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     names.sort(); | ||||||
|  |  | ||||||
|  |     res.send(names); | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | router.get('/attributes/values/:attributeName', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|  |     const attributeName = req.params.attributeName; | ||||||
|  |  | ||||||
|  |     const values = await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE name = ? AND value != '' ORDER BY value", [attributeName]); | ||||||
|  |  | ||||||
|  |     res.send(values); | ||||||
|  | })); | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
| @@ -58,15 +58,112 @@ router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | |||||||
| })); | })); | ||||||
|  |  | ||||||
| router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { | router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const search = '%' + utils.sanitizeSql(req.query.search) + '%'; |     let {attrFilters, searchText} = parseFilters(req.query.search); | ||||||
|  |  | ||||||
|     // searching in protected notes is pointless because of encryption |     const {query, params} = getSearchQuery(attrFilters, searchText); | ||||||
|     const noteIds = await sql.getColumn(`SELECT noteId FROM notes  |  | ||||||
|               WHERE isDeleted = 0 AND isProtected = 0 AND (title LIKE ? OR content LIKE ?)`, [search, search]); |     const noteIds = await sql.getColumn(query, params); | ||||||
|  |  | ||||||
|     res.send(noteIds); |     res.send(noteIds); | ||||||
| })); | })); | ||||||
|  |  | ||||||
|  | function parseFilters(searchText) { | ||||||
|  |     const attrFilters = []; | ||||||
|  |  | ||||||
|  |     const attrRegex = /(\b(and|or)\s+)?@(!?)([\w_-]+|"[^"]+")((=|!=|<|<=|>|>=)([\w_-]+|"[^"]+"))?/i; | ||||||
|  |  | ||||||
|  |     let match = attrRegex.exec(searchText); | ||||||
|  |  | ||||||
|  |     function trimQuotes(str) { return str.startsWith('"') ? str.substr(1, str.length - 2) : str; } | ||||||
|  |  | ||||||
|  |     while (match != null) { | ||||||
|  |         const relation = match[2] !== undefined ? match[2].toLowerCase() : 'and'; | ||||||
|  |         const operator = match[3] === '!' ? 'not-exists' : 'exists'; | ||||||
|  |  | ||||||
|  |         attrFilters.push({ | ||||||
|  |             relation: relation, | ||||||
|  |             name: trimQuotes(match[4]), | ||||||
|  |             operator: match[6] !== undefined ? match[6] : operator, | ||||||
|  |             value: match[7] !== undefined ? trimQuotes(match[7]) : null | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // remove attributes from further fulltext search | ||||||
|  |         searchText = searchText.split(match[0]).join(''); | ||||||
|  |  | ||||||
|  |         match = attrRegex.exec(searchText); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return {attrFilters, searchText}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getSearchQuery(attrFilters, searchText) { | ||||||
|  |     const joins = []; | ||||||
|  |     const joinParams = []; | ||||||
|  |     let where = '1'; | ||||||
|  |     const whereParams = []; | ||||||
|  |  | ||||||
|  |     let i = 1; | ||||||
|  |  | ||||||
|  |     for (const filter of attrFilters) { | ||||||
|  |         joins.push(`LEFT JOIN attributes AS attr${i} ON attr${i}.noteId = notes.noteId AND attr${i}.name = ?`); | ||||||
|  |         joinParams.push(filter.name); | ||||||
|  |  | ||||||
|  |         where += " " + filter.relation + " "; | ||||||
|  |  | ||||||
|  |         if (filter.operator === 'exists') { | ||||||
|  |             where += `attr${i}.attributeId IS NOT NULL`; | ||||||
|  |         } | ||||||
|  |         else if (filter.operator === 'not-exists') { | ||||||
|  |             where += `attr${i}.attributeId IS NULL`; | ||||||
|  |         } | ||||||
|  |         else if (filter.operator === '=' || filter.operator === '!=') { | ||||||
|  |             where += `attr${i}.value ${filter.operator} ?`; | ||||||
|  |             whereParams.push(filter.value); | ||||||
|  |         } | ||||||
|  |         else if ([">", ">=", "<", "<="].includes(filter.operator)) { | ||||||
|  |             const floatParam = parseFloat(filter.value); | ||||||
|  |  | ||||||
|  |             if (isNaN(floatParam)) { | ||||||
|  |                 where += `attr${i}.value ${filter.operator} ?`; | ||||||
|  |                 whereParams.push(filter.value); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 where += `CAST(attr${i}.value AS DECIMAL) ${filter.operator} ?`; | ||||||
|  |                 whereParams.push(floatParam); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             throw new Error("Unknown operator " + filter.operator); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         i++; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let searchCondition = ''; | ||||||
|  |     const searchParams = []; | ||||||
|  |  | ||||||
|  |     if (searchText.trim() !== '') { | ||||||
|  |         // searching in protected notes is pointless because of encryption | ||||||
|  |         searchCondition = ' AND (notes.isProtected = 0 AND (notes.title LIKE ? OR notes.content LIKE ?))'; | ||||||
|  |  | ||||||
|  |         searchText = '%' + searchText.trim() + '%'; | ||||||
|  |  | ||||||
|  |         searchParams.push(searchText); | ||||||
|  |         searchParams.push(searchText); // two occurences in searchCondition | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const query = `SELECT notes.noteId FROM notes | ||||||
|  |             ${joins.join('\r\n')} | ||||||
|  |               WHERE  | ||||||
|  |                 notes.isDeleted = 0 | ||||||
|  |                 AND (${where})  | ||||||
|  |                 ${searchCondition}`; | ||||||
|  |  | ||||||
|  |     const params = joinParams.concat(whereParams).concat(searchParams); | ||||||
|  |  | ||||||
|  |     return { query, params }; | ||||||
|  | } | ||||||
|  |  | ||||||
| router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => { | router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|     const sourceId = req.headers.source_id; |     const sourceId = req.headers.source_id; | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ function register(app) { | |||||||
|     app.use('/api/notes', notesApiRoute); |     app.use('/api/notes', notesApiRoute); | ||||||
|     app.use('/api/tree', treeChangesApiRoute); |     app.use('/api/tree', treeChangesApiRoute); | ||||||
|     app.use('/api/notes', cloningApiRoute); |     app.use('/api/notes', cloningApiRoute); | ||||||
|     app.use('/api/notes', attributesRoute); |     app.use('/api', attributesRoute); | ||||||
|     app.use('/api/notes-history', noteHistoryApiRoute); |     app.use('/api/notes-history', noteHistoryApiRoute); | ||||||
|     app.use('/api/recent-changes', recentChangesApiRoute); |     app.use('/api/recent-changes', recentChangesApiRoute); | ||||||
|     app.use('/api/settings', settingsApiRoute); |     app.use('/api/settings', settingsApiRoute); | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ const utils = require('./utils'); | |||||||
| const sync_table = require('./sync_table'); | const sync_table = require('./sync_table'); | ||||||
| const Repository = require('./repository'); | const Repository = require('./repository'); | ||||||
|  |  | ||||||
|  | const BUILTIN_ATTRIBUTES = [ 'run_on_startup', 'disable_versioning' ]; | ||||||
|  |  | ||||||
| async function getNoteAttributeMap(noteId) { | async function getNoteAttributeMap(noteId) { | ||||||
|     return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]); |     return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]); | ||||||
| } | } | ||||||
| @@ -64,5 +66,6 @@ module.exports = { | |||||||
|     getNotesWithAttribute, |     getNotesWithAttribute, | ||||||
|     getNoteWithAttribute, |     getNoteWithAttribute, | ||||||
|     getNoteIdsWithAttribute, |     getNoteIdsWithAttribute, | ||||||
|     createAttribute |     createAttribute, | ||||||
|  |     BUILTIN_ATTRIBUTES | ||||||
| }; | }; | ||||||
| @@ -56,14 +56,13 @@ | |||||||
|             <img src="images/icons/search.png" alt="Search in notes"/> |             <img src="images/icons/search.png" alt="Search in notes"/> | ||||||
|           </a> |           </a> | ||||||
|         </div> |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|         <div id="search-box" style="display: none; padding: 10px; margin-top: 10px;"> |       <div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;"> | ||||||
|           <p> |         <div style="display: flex; align-items: center;"> | ||||||
|             <label>Search:</label> |           <label>Search:</label> | ||||||
|             <input name="search-text" autocomplete="off"> |           <input name="search-text" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off"> | ||||||
|             <button id="reset-search-button">×</button> |           <button id="reset-search-button" class="btn btn-sm" title="Reset search">×</button> | ||||||
|             <span id="matches"></span> |  | ||||||
|           </p> |  | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
| @@ -73,7 +72,7 @@ | |||||||
|       <div id="parent-list" class="hide-toggle"> |       <div id="parent-list" class="hide-toggle"> | ||||||
|         <p><strong>Note locations:</strong></p> |         <p><strong>Note locations:</strong></p> | ||||||
|  |  | ||||||
|         <ul id="parent-list-list"></ul> |         <ul id="parent-list-inner"></ul> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div class="hide-toggle" style="grid-area: title;"> |       <div class="hide-toggle" style="grid-area: title;"> | ||||||
| @@ -143,6 +142,12 @@ | |||||||
|  |  | ||||||
|         <div id="note-detail-render"></div> |         <div id="note-detail-render"></div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|  |       <div id="attribute-list"> | ||||||
|  |         <button class="btn btn-sm" onclick="attributesDialog.showDialog();">Attributes:</button> | ||||||
|  |  | ||||||
|  |         <span id="attribute-list-inner"></span> | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div id="recent-notes-dialog" title="Recent notes" style="display: none;"> |     <div id="recent-notes-dialog" title="Recent notes" style="display: none;"> | ||||||
| @@ -383,10 +388,8 @@ | |||||||
|  |  | ||||||
|     <div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;"> |     <div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;"> | ||||||
|       <form data-bind="submit: save"> |       <form data-bind="submit: save"> | ||||||
|       <div style="display: flex; justify-content: space-between; padding: 15px; padding-top: 0;"> |       <div style="text-align: center"> | ||||||
|         <button class="btn-default" type="button" data-bind="click: addNewRow">Add new attribute</button> |         <button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save <kbd>enter</kbd></button> | ||||||
|  |  | ||||||
|         <button class="btn-primary" type="submit">Save</button> |  | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div style="height: 97%; overflow: auto"> |       <div style="height: 97%; overflow: auto"> | ||||||
| @@ -402,10 +405,14 @@ | |||||||
|             <tr> |             <tr> | ||||||
|               <td data-bind="text: attributeId"></td> |               <td data-bind="text: attributeId"></td> | ||||||
|               <td> |               <td> | ||||||
|                 <input type="text" data-bind="value: name"/> |                 <!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event --> | ||||||
|  |                 <input type="text" class="attribute-name" data-bind="value: name, valueUpdate: 'blur',  event: { blur: $parent.attributeChanged }"/> | ||||||
|  |  | ||||||
|  |                 <div style="color: red" data-bind="if: $parent.isNotUnique($index())">Attribute name must be unique per note.</div> | ||||||
|  |                 <div style="color: red" data-bind="if: $parent.isEmptyName($index())">Attribute name can't be empty.</div> | ||||||
|               </td> |               </td> | ||||||
|               <td> |               <td> | ||||||
|                 <input type="text" data-bind="value: value" style="width: 300px"/> |                 <input type="text" class="attribute-value" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/> | ||||||
|               </td> |               </td> | ||||||
|             </tr> |             </tr> | ||||||
|           </tbody> |           </tbody> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user