mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 03:29:02 +01:00 
			
		
		
		
	added "type" to attribute dialog, name autocomplete servers according to the choice
This commit is contained in:
		
							parent
							
								
									097114c0f2
								
							
						
					
					
						commit
						509093b755
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										239
									
								
								src/public/javascripts/dialogs/attributes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								src/public/javascripts/dialogs/attributes.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,239 @@ | ||||
| import noteDetailService from '../services/note_detail.js'; | ||||
| import server from '../services/server.js'; | ||||
| import infoService from "../services/info.js"; | ||||
| 
 | ||||
| const $dialog = $("#attributes-dialog"); | ||||
| const $saveAttributesButton = $("#save-attributes-button"); | ||||
| const $attributesBody = $('#attributes-table tbody'); | ||||
| 
 | ||||
| const attributesModel = new AttributesModel(); | ||||
| 
 | ||||
| function AttributesModel() { | ||||
|     const self = this; | ||||
| 
 | ||||
|     this.attributes = ko.observableArray(); | ||||
| 
 | ||||
|     this.availableTypes = [ | ||||
|         { text: "Label", value: "label" }, | ||||
|         { text: "Label definition", value: "definition" }, | ||||
|         { text: "Relation", value: "relation" } | ||||
|     ]; | ||||
| 
 | ||||
|     this.updateAttributePositions = function() { | ||||
|         let position = 0; | ||||
| 
 | ||||
|         // we need to update positions by searching in the DOM, because order of the
 | ||||
|         // attributes in the viewmodel (self.attributes()) stays the same
 | ||||
|         $attributesBody.find('input[name="position"]').each(function() { | ||||
|             const attribute = self.getTargetAttribute(this); | ||||
| 
 | ||||
|             attribute().position = position++; | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     this.loadAttributes = async function() { | ||||
|         const noteId = noteDetailService.getCurrentNoteId(); | ||||
| 
 | ||||
|         const attributes = await server.get('notes/' + noteId + '/attributes'); | ||||
| 
 | ||||
|         self.attributes(attributes.map(ko.observable)); | ||||
| 
 | ||||
|         addLastEmptyRow(); | ||||
| 
 | ||||
|         // attribute might not be rendered immediatelly so could not focus
 | ||||
|         setTimeout(() => $(".attribute-name:last").focus(), 100); | ||||
| 
 | ||||
|         $attributesBody.sortable({ | ||||
|             handle: '.handle', | ||||
|             containment: $attributesBody, | ||||
|             update: this.updateAttributePositions | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     this.deleteAttribute = function(data, event) { | ||||
|         const attribute = self.getTargetAttribute(event.target); | ||||
|         const attributeData = attribute(); | ||||
| 
 | ||||
|         if (attributeData) { | ||||
|             attributeData.isDeleted = 1; | ||||
| 
 | ||||
|             attribute(attributeData); | ||||
| 
 | ||||
|             addLastEmptyRow(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     function isValid() { | ||||
|         for (let attributes = self.attributes(), i = 0; i < attributes.length; i++) { | ||||
|             if (self.isEmptyName(i)) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     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; | ||||
|         } | ||||
| 
 | ||||
|         self.updateAttributePositions(); | ||||
| 
 | ||||
|         const noteId = noteDetailService.getCurrentNoteId(); | ||||
| 
 | ||||
|         const attributesToSave = self.attributes() | ||||
|             .map(attribute => attribute()) | ||||
|             .filter(attribute => attribute.attributeId !== "" || attribute.name !== ""); | ||||
| 
 | ||||
|         const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave); | ||||
| 
 | ||||
|         self.attributes(attributes.map(ko.observable)); | ||||
| 
 | ||||
|         addLastEmptyRow(); | ||||
| 
 | ||||
|         infoService.showMessage("Attributes have been saved."); | ||||
| 
 | ||||
|         noteDetailService.loadAttributeList(); | ||||
|     }; | ||||
| 
 | ||||
|     function addLastEmptyRow() { | ||||
|         const attributes = self.attributes().filter(attr => attr().isDeleted === 0); | ||||
|         const last = attributes.length === 0 ? null : attributes[attributes.length - 1](); | ||||
| 
 | ||||
|         if (!last || last.name.trim() !== "" || last.value !== "") { | ||||
|             self.attributes.push(ko.observable({ | ||||
|                 attributeId: '', | ||||
|                 type: 'label', | ||||
|                 name: '', | ||||
|                 value: '', | ||||
|                 isDeleted: 0, | ||||
|                 position: 0 | ||||
|             })); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     this.attributeChanged = function (data, event) { | ||||
|         addLastEmptyRow(); | ||||
| 
 | ||||
|         const attribute = self.getTargetAttribute(event.target); | ||||
| 
 | ||||
|         attribute.valueHasMutated(); | ||||
|     }; | ||||
| 
 | ||||
|     this.isNotUnique = function(index) { | ||||
|         const cur = self.attributes()[index](); | ||||
| 
 | ||||
|         if (cur.name.trim() === "") { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         for (let attributes = self.attributes(), i = 0; i < attributes.length; i++) { | ||||
|             const attribute = attributes[i](); | ||||
| 
 | ||||
|             if (index !== i && cur.name === attribute.name) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     }; | ||||
| 
 | ||||
|     this.isEmptyName = function(index) { | ||||
|         const cur = self.attributes()[index](); | ||||
| 
 | ||||
|         return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== ""); | ||||
|     }; | ||||
| 
 | ||||
|     this.getTargetAttribute = function(target) { | ||||
|         const context = ko.contextFor(target); | ||||
|         const index = context.$index(); | ||||
| 
 | ||||
|         return self.attributes()[index]; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async function showDialog() { | ||||
|     glob.activeDialog = $dialog; | ||||
| 
 | ||||
|     await attributesModel.loadAttributes(); | ||||
| 
 | ||||
|     $dialog.dialog({ | ||||
|         modal: true, | ||||
|         width: 950, | ||||
|         height: 500 | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| ko.applyBindings(attributesModel, $dialog[0]); | ||||
| 
 | ||||
| $dialog.on('focus', '.attribute-name', function (e) { | ||||
|     if (!$(this).hasClass("ui-autocomplete-input")) { | ||||
|         $(this).autocomplete({ | ||||
|             source: async (request, response) => { | ||||
|                 const attribute = attributesModel.getTargetAttribute(this); | ||||
|                 const type = attribute().type === 'relation' ? 'relation' : 'label'; | ||||
|                 const names = await server.get('attributes/names/?type=' + type + '&query=' + encodeURIComponent(request.term)); | ||||
|                 const result = names.map(name => { | ||||
|                     return { | ||||
|                         label: name, | ||||
|                         value: name | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 if (result.length > 0) { | ||||
|                     response(result); | ||||
|                 } | ||||
|                 else { | ||||
|                     response([{ | ||||
|                         label: "No results", | ||||
|                         value: "No results" | ||||
|                     }]); | ||||
|                 } | ||||
|             }, | ||||
|             minLength: 0 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     $(this).autocomplete("search", $(this).val()); | ||||
| }); | ||||
| 
 | ||||
| $dialog.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 autocomplete.js
 | ||||
|             source: attributeValues.map(attribute => { | ||||
|                 return { | ||||
|                     attribute: attribute, | ||||
|                     value: attribute | ||||
|                 } | ||||
|             }), | ||||
|             minLength: 0 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     $(this).autocomplete("search", $(this).val()); | ||||
| }); | ||||
| 
 | ||||
| export default { | ||||
|     showDialog | ||||
| }; | ||||
							
								
								
									
										1
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,7 @@ | ||||
| import addLinkDialog from '../dialogs/add_link.js'; | ||||
| import jumpToNoteDialog from '../dialogs/jump_to_note.js'; | ||||
| import labelsDialog from '../dialogs/labels.js'; | ||||
| import attributesDialog from '../dialogs/attributes.js'; | ||||
| import noteRevisionsDialog from '../dialogs/note_revisions.js'; | ||||
| import noteSourceDialog from '../dialogs/note_source.js'; | ||||
| import recentChangesDialog from '../dialogs/recent_changes.js'; | ||||
|  | ||||
| @ -11,6 +11,7 @@ import noteSourceDialog from "../dialogs/note_source.js"; | ||||
| import recentChangesDialog from "../dialogs/recent_changes.js"; | ||||
| import sqlConsoleDialog from "../dialogs/sql_console.js"; | ||||
| import searchNotesService from "./search_notes.js"; | ||||
| import attributesDialog from "../dialogs/attributes.js"; | ||||
| import labelsDialog from "../dialogs/labels.js"; | ||||
| import relationsDialog from "../dialogs/relations.js"; | ||||
| import protectedSessionService from "./protected_session.js"; | ||||
| @ -38,6 +39,9 @@ function registerEntrypoints() { | ||||
|     $("#toggle-search-button").click(searchNotesService.toggleSearch); | ||||
|     utils.bindShortcut('ctrl+s', searchNotesService.toggleSearch); | ||||
| 
 | ||||
|     $(".show-attributes-button").click(attributesDialog.showDialog); | ||||
|     utils.bindShortcut('alt+a', attributesDialog.showDialog); | ||||
| 
 | ||||
|     $(".show-labels-button").click(labelsDialog.showDialog); | ||||
|     utils.bindShortcut('alt+l', labelsDialog.showDialog); | ||||
| 
 | ||||
|  | ||||
| @ -42,18 +42,11 @@ async function updateNoteAttributes(req) { | ||||
|     return await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]); | ||||
| } | ||||
| 
 | ||||
| async function getAllAttributeNames() { | ||||
|     const names = await sql.getColumn("SELECT DISTINCT name FROM attributes WHERE isDeleted = 0"); | ||||
| async function getAttributeNames(req) { | ||||
|     const type = req.query.type; | ||||
|     const query = req.query.query; | ||||
| 
 | ||||
|     for (const attribute of attributeService.BUILTIN_ATTRIBUTES) { | ||||
|         if (!names.includes(attribute)) { | ||||
|             names.push(attribute); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     names.sort(); | ||||
| 
 | ||||
|     return names; | ||||
|     return attributeService.getAttributeNames(type, query); | ||||
| } | ||||
| 
 | ||||
| async function getValuesForAttribute(req) { | ||||
| @ -65,6 +58,6 @@ async function getValuesForAttribute(req) { | ||||
| module.exports = { | ||||
|     getNoteAttributes, | ||||
|     updateNoteAttributes, | ||||
|     getAllAttributeNames, | ||||
|     getAttributeNames, | ||||
|     getValuesForAttribute | ||||
| }; | ||||
| @ -136,7 +136,7 @@ function register(app) { | ||||
| 
 | ||||
|     apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getNoteAttributes); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/attributes', attributesRoute.updateNoteAttributes); | ||||
|     apiRoute(GET, '/api/attributes/names', attributesRoute.getAllAttributeNames); | ||||
|     apiRoute(GET, '/api/attributes/names', attributesRoute.getAttributeNames); | ||||
|     apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); | ||||
| 
 | ||||
|     apiRoute(GET, '/api/notes/:noteId/labels', labelsRoute.getNoteLabels); | ||||
|  | ||||
| @ -1,18 +1,25 @@ | ||||
| "use strict"; | ||||
| 
 | ||||
| const repository = require('./repository'); | ||||
| const sql = require('./sql'); | ||||
| const utils = require('./utils'); | ||||
| const Attribute = require('../entities/attribute'); | ||||
| 
 | ||||
| const BUILTIN_ATTRIBUTES = [ | ||||
|     'disableVersioning', | ||||
|     'calendarRoot', | ||||
|     'archived', | ||||
|     'excludeFromExport', | ||||
|     'run', | ||||
|     'manualTransactionHandling', | ||||
|     'disableInclusion', | ||||
|     'appCss', | ||||
|     'hideChildrenOverview' | ||||
|     // label names
 | ||||
|     { type: 'label', name: 'disableVersioning' }, | ||||
|     { type: 'label', name: 'calendarRoot' }, | ||||
|     { type: 'label', name: 'archived' }, | ||||
|     { type: 'label', name: 'excludeFromExport' }, | ||||
|     { type: 'label', name: 'run' }, | ||||
|     { type: 'label', name: 'manualTransactionHandling' }, | ||||
|     { type: 'label', name: 'disableInclusion' }, | ||||
|     { type: 'label', name: 'appCss' }, | ||||
|     { type: 'label', name: 'hideChildrenOverview' }, | ||||
| 
 | ||||
|     // relation names
 | ||||
|     { type: 'relation', name: 'runOnNoteView' }, | ||||
|     { type: 'relation', name: 'runOnNoteTitleChange' } | ||||
| ]; | ||||
| 
 | ||||
| async function getNotesWithAttribute(name, value) { | ||||
| @ -44,9 +51,29 @@ async function createAttribute(noteId, name, value = "") { | ||||
|     }).save(); | ||||
| } | ||||
| 
 | ||||
| async function getAttributeNames(type, nameLike) { | ||||
|     const names = await sql.getColumn( | ||||
|         `SELECT DISTINCT name 
 | ||||
|          FROM attributes  | ||||
|          WHERE isDeleted = 0 | ||||
|            AND type = ? | ||||
|            AND name LIKE '%${utils.sanitizeSql(nameLike)}%'`, [ type ]);
 | ||||
| 
 | ||||
|     for (const attribute of BUILTIN_ATTRIBUTES) { | ||||
|         if (attribute.type === type && !names.includes(attribute.name)) { | ||||
|             names.push(attribute.name); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     names.sort(); | ||||
| 
 | ||||
|     return names; | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|     getNotesWithAttribute, | ||||
|     getNoteWithAttribute, | ||||
|     createAttribute, | ||||
|     getAttributeNames, | ||||
|     BUILTIN_ATTRIBUTES | ||||
| }; | ||||
| @ -169,6 +169,7 @@ | ||||
|               </button> | ||||
|               <ul class="dropdown-menu dropdown-menu-right"> | ||||
|                 <li><a id="show-note-revisions-button">Note revisions</a></li> | ||||
|                 <li><a class="show-attributes-button"><kbd>Alt+A</kbd> Attributes</a></li> | ||||
|                 <li><a class="show-labels-button"><kbd>Alt+L</kbd> Labels</a></li> | ||||
|                 <li><a class="show-relations-button"><kbd>Alt+R</kbd> Relations</a></li> | ||||
|                 <li><a id="show-source-button">HTML source</a></li> | ||||
| @ -554,6 +555,53 @@ | ||||
|       <textarea id="note-source" readonly="readonly"></textarea> | ||||
|     </div> | ||||
| 
 | ||||
|     <div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;"> | ||||
|       <form data-bind="submit: save"> | ||||
|         <div style="text-align: center"> | ||||
|           <button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save changes <kbd>enter</kbd></button> | ||||
|         </div> | ||||
| 
 | ||||
|         <div style="height: 97%; overflow: auto"> | ||||
|           <table id="attributes-table" class="table"> | ||||
|             <thead> | ||||
|             <tr> | ||||
|               <th></th> | ||||
|               <th>ID</th> | ||||
|               <th>Name</th> | ||||
|               <th>Value</th> | ||||
|               <th></th> | ||||
|             </tr> | ||||
|             </thead> | ||||
|             <tbody data-bind="foreach: attributes"> | ||||
|             <tr data-bind="if: isDeleted == 0"> | ||||
|               <td class="handle"> | ||||
|                 <span class="glyphicon glyphicon-resize-vertical"></span> | ||||
|                 <input type="hidden" name="position" data-bind="value: position"/> | ||||
|               </td> | ||||
|               <!-- ID column has specific width because if it's empty its size can be deformed when dragging --> | ||||
|               <td data-bind="text: attributeId" style="min-width: 10em; font-size: smaller;"></td> | ||||
|               <td> | ||||
|                   <select data-bind="options: $root.availableTypes, optionsText: 'text', optionsValue: 'value', value: type"></select> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event --> | ||||
|                 <input type="text" class="attribute-name form-control" data-bind="value: name, valueUpdate: 'blur',  event: { blur: $parent.attributeChanged }"/> | ||||
|                 <div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate attribute.</div> | ||||
|                 <div style="color: red" data-bind="if: $parent.isEmptyName($index())">Attribute name can't be empty.</div> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <input type="text" class="attribute-value form-control" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/> | ||||
|               </td> | ||||
|               <td title="Delete" style="padding: 13px; cursor: pointer;"> | ||||
|                 <span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteAttribute"></span> | ||||
|               </td> | ||||
|             </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       </form> | ||||
|     </div> | ||||
| 
 | ||||
|     <div id="labels-dialog" title="Note labels" style="display: none; padding: 20px;"> | ||||
|       <form data-bind="submit: save"> | ||||
|       <div style="text-align: center"> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 azivner
						azivner