mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	added "type" to attribute dialog, name autocomplete servers according to the choice
This commit is contained in:
		
										
											
												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 addLinkDialog from '../dialogs/add_link.js'; | ||||||
| import jumpToNoteDialog from '../dialogs/jump_to_note.js'; | import jumpToNoteDialog from '../dialogs/jump_to_note.js'; | ||||||
| import labelsDialog from '../dialogs/labels.js'; | import labelsDialog from '../dialogs/labels.js'; | ||||||
|  | import attributesDialog from '../dialogs/attributes.js'; | ||||||
| import noteRevisionsDialog from '../dialogs/note_revisions.js'; | import noteRevisionsDialog from '../dialogs/note_revisions.js'; | ||||||
| import noteSourceDialog from '../dialogs/note_source.js'; | import noteSourceDialog from '../dialogs/note_source.js'; | ||||||
| import recentChangesDialog from '../dialogs/recent_changes.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 recentChangesDialog from "../dialogs/recent_changes.js"; | ||||||
| import sqlConsoleDialog from "../dialogs/sql_console.js"; | import sqlConsoleDialog from "../dialogs/sql_console.js"; | ||||||
| import searchNotesService from "./search_notes.js"; | import searchNotesService from "./search_notes.js"; | ||||||
|  | import attributesDialog from "../dialogs/attributes.js"; | ||||||
| import labelsDialog from "../dialogs/labels.js"; | import labelsDialog from "../dialogs/labels.js"; | ||||||
| import relationsDialog from "../dialogs/relations.js"; | import relationsDialog from "../dialogs/relations.js"; | ||||||
| import protectedSessionService from "./protected_session.js"; | import protectedSessionService from "./protected_session.js"; | ||||||
| @@ -38,6 +39,9 @@ function registerEntrypoints() { | |||||||
|     $("#toggle-search-button").click(searchNotesService.toggleSearch); |     $("#toggle-search-button").click(searchNotesService.toggleSearch); | ||||||
|     utils.bindShortcut('ctrl+s', 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); |     $(".show-labels-button").click(labelsDialog.showDialog); | ||||||
|     utils.bindShortcut('alt+l', 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]); |     return await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getAllAttributeNames() { | async function getAttributeNames(req) { | ||||||
|     const names = await sql.getColumn("SELECT DISTINCT name FROM attributes WHERE isDeleted = 0"); |     const type = req.query.type; | ||||||
|  |     const query = req.query.query; | ||||||
|  |  | ||||||
|     for (const attribute of attributeService.BUILTIN_ATTRIBUTES) { |     return attributeService.getAttributeNames(type, query); | ||||||
|         if (!names.includes(attribute)) { |  | ||||||
|             names.push(attribute); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     names.sort(); |  | ||||||
|  |  | ||||||
|     return names; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getValuesForAttribute(req) { | async function getValuesForAttribute(req) { | ||||||
| @@ -65,6 +58,6 @@ async function getValuesForAttribute(req) { | |||||||
| module.exports = { | module.exports = { | ||||||
|     getNoteAttributes, |     getNoteAttributes, | ||||||
|     updateNoteAttributes, |     updateNoteAttributes, | ||||||
|     getAllAttributeNames, |     getAttributeNames, | ||||||
|     getValuesForAttribute |     getValuesForAttribute | ||||||
| }; | }; | ||||||
| @@ -136,7 +136,7 @@ function register(app) { | |||||||
|  |  | ||||||
|     apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getNoteAttributes); |     apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getNoteAttributes); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/attributes', attributesRoute.updateNoteAttributes); |     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/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/notes/:noteId/labels', labelsRoute.getNoteLabels); |     apiRoute(GET, '/api/notes/:noteId/labels', labelsRoute.getNoteLabels); | ||||||
|   | |||||||
| @@ -1,18 +1,25 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const repository = require('./repository'); | const repository = require('./repository'); | ||||||
|  | const sql = require('./sql'); | ||||||
|  | const utils = require('./utils'); | ||||||
| const Attribute = require('../entities/attribute'); | const Attribute = require('../entities/attribute'); | ||||||
|  |  | ||||||
| const BUILTIN_ATTRIBUTES = [ | const BUILTIN_ATTRIBUTES = [ | ||||||
|     'disableVersioning', |     // label names | ||||||
|     'calendarRoot', |     { type: 'label', name: 'disableVersioning' }, | ||||||
|     'archived', |     { type: 'label', name: 'calendarRoot' }, | ||||||
|     'excludeFromExport', |     { type: 'label', name: 'archived' }, | ||||||
|     'run', |     { type: 'label', name: 'excludeFromExport' }, | ||||||
|     'manualTransactionHandling', |     { type: 'label', name: 'run' }, | ||||||
|     'disableInclusion', |     { type: 'label', name: 'manualTransactionHandling' }, | ||||||
|     'appCss', |     { type: 'label', name: 'disableInclusion' }, | ||||||
|     'hideChildrenOverview' |     { type: 'label', name: 'appCss' }, | ||||||
|  |     { type: 'label', name: 'hideChildrenOverview' }, | ||||||
|  |  | ||||||
|  |     // relation names | ||||||
|  |     { type: 'relation', name: 'runOnNoteView' }, | ||||||
|  |     { type: 'relation', name: 'runOnNoteTitleChange' } | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| async function getNotesWithAttribute(name, value) { | async function getNotesWithAttribute(name, value) { | ||||||
| @@ -44,9 +51,29 @@ async function createAttribute(noteId, name, value = "") { | |||||||
|     }).save(); |     }).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 = { | module.exports = { | ||||||
|     getNotesWithAttribute, |     getNotesWithAttribute, | ||||||
|     getNoteWithAttribute, |     getNoteWithAttribute, | ||||||
|     createAttribute, |     createAttribute, | ||||||
|  |     getAttributeNames, | ||||||
|     BUILTIN_ATTRIBUTES |     BUILTIN_ATTRIBUTES | ||||||
| }; | }; | ||||||
| @@ -169,6 +169,7 @@ | |||||||
|               </button> |               </button> | ||||||
|               <ul class="dropdown-menu dropdown-menu-right"> |               <ul class="dropdown-menu dropdown-menu-right"> | ||||||
|                 <li><a id="show-note-revisions-button">Note revisions</a></li> |                 <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-labels-button"><kbd>Alt+L</kbd> Labels</a></li> | ||||||
|                 <li><a class="show-relations-button"><kbd>Alt+R</kbd> Relations</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> |                 <li><a id="show-source-button">HTML source</a></li> | ||||||
| @@ -554,6 +555,53 @@ | |||||||
|       <textarea id="note-source" readonly="readonly"></textarea> |       <textarea id="note-source" readonly="readonly"></textarea> | ||||||
|     </div> |     </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;"> |     <div id="labels-dialog" title="Note labels" style="display: none; padding: 20px;"> | ||||||
|       <form data-bind="submit: save"> |       <form data-bind="submit: save"> | ||||||
|       <div style="text-align: center"> |       <div style="text-align: center"> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user