mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-29 11:44:21 +08:00 
			
		
		
		
	cleanup of labels & relations frontend code
This commit is contained in:
		
							parent
							
								
									5f36856571
								
							
						
					
					
						commit
						3491235533
					
				| @ -1,222 +0,0 @@ | ||||
| import noteDetailService from '../services/note_detail.js'; | ||||
| import server from '../services/server.js'; | ||||
| import infoService from "../services/info.js"; | ||||
| 
 | ||||
| const $dialog = $("#labels-dialog"); | ||||
| const $saveLabelsButton = $("#save-labels-button"); | ||||
| const $labelsBody = $('#labels-table tbody'); | ||||
| 
 | ||||
| const labelsModel = new LabelsModel(); | ||||
| let labelNames = []; | ||||
| 
 | ||||
| function LabelsModel() { | ||||
|     const self = this; | ||||
| 
 | ||||
|     this.labels = ko.observableArray(); | ||||
| 
 | ||||
|     this.updateLabelPositions = function() { | ||||
|         let position = 0; | ||||
| 
 | ||||
|         // we need to update positions by searching in the DOM, because order of the
 | ||||
|         // labels in the viewmodel (self.labels()) stays the same
 | ||||
|         $labelsBody.find('input[name="position"]').each(function() { | ||||
|             const label = self.getTargetLabel(this); | ||||
| 
 | ||||
|             label().position = position++; | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     this.loadLabels = async function() { | ||||
|         const noteId = noteDetailService.getCurrentNoteId(); | ||||
| 
 | ||||
|         const labels = await server.get('notes/' + noteId + '/labels'); | ||||
| 
 | ||||
|         self.labels(labels.map(ko.observable)); | ||||
| 
 | ||||
|         addLastEmptyRow(); | ||||
| 
 | ||||
|         labelNames = await server.get('labels/names'); | ||||
| 
 | ||||
|         // label might not be rendered immediatelly so could not focus
 | ||||
|         setTimeout(() => $(".label-name:last").focus(), 100); | ||||
| 
 | ||||
|         $labelsBody.sortable({ | ||||
|             handle: '.handle', | ||||
|             containment: $labelsBody, | ||||
|             update: this.updateLabelPositions | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     this.deleteLabel = function(data, event) { | ||||
|         const label = self.getTargetLabel(event.target); | ||||
|         const labelData = label(); | ||||
| 
 | ||||
|         if (labelData) { | ||||
|             labelData.isDeleted = true; | ||||
| 
 | ||||
|             label(labelData); | ||||
| 
 | ||||
|             addLastEmptyRow(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     function isValid() { | ||||
|         for (let labels = self.labels(), i = 0; i < labels.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.
 | ||||
|         $saveLabelsButton.focus(); | ||||
| 
 | ||||
|         if (!isValid()) { | ||||
|             alert("Please fix all validation errors and try saving again."); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         self.updateLabelPositions(); | ||||
| 
 | ||||
|         const noteId = noteDetailService.getCurrentNoteId(); | ||||
| 
 | ||||
|         const labelsToSave = self.labels() | ||||
|             .map(label => label()) | ||||
|             .filter(label => label.labelId !== "" || label.name !== ""); | ||||
| 
 | ||||
|         const labels = await server.put('notes/' + noteId + '/labels', labelsToSave); | ||||
| 
 | ||||
|         self.labels(labels.map(ko.observable)); | ||||
| 
 | ||||
|         addLastEmptyRow(); | ||||
| 
 | ||||
|         infoService.showMessage("Labels have been saved."); | ||||
| 
 | ||||
|         noteDetailService.loadLabelList(); | ||||
|     }; | ||||
| 
 | ||||
|     function addLastEmptyRow() { | ||||
|         const labels = self.labels().filter(attr => !attr().isDeleted); | ||||
|         const last = labels.length === 0 ? null : labels[labels.length - 1](); | ||||
| 
 | ||||
|         if (!last || last.name.trim() !== "" || last.value !== "") { | ||||
|             self.labels.push(ko.observable({ | ||||
|                 labelId: '', | ||||
|                 name: '', | ||||
|                 value: '', | ||||
|                 isDeleted: false, | ||||
|                 position: 0 | ||||
|             })); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     this.labelChanged = function (data, event) { | ||||
|         addLastEmptyRow(); | ||||
| 
 | ||||
|         const label = self.getTargetLabel(event.target); | ||||
| 
 | ||||
|         label.valueHasMutated(); | ||||
|     }; | ||||
| 
 | ||||
|     this.isNotUnique = function(index) { | ||||
|         const cur = self.labels()[index](); | ||||
| 
 | ||||
|         if (cur.name.trim() === "") { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         for (let labels = self.labels(), i = 0; i < labels.length; i++) { | ||||
|             const label = labels[i](); | ||||
| 
 | ||||
|             if (index !== i && cur.name === label.name) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     }; | ||||
| 
 | ||||
|     this.isEmptyName = function(index) { | ||||
|         const cur = self.labels()[index](); | ||||
| 
 | ||||
|         return cur.name.trim() === "" && (cur.labelId !== "" || cur.value !== ""); | ||||
|     }; | ||||
| 
 | ||||
|     this.getTargetLabel = function(target) { | ||||
|         const context = ko.contextFor(target); | ||||
|         const index = context.$index(); | ||||
| 
 | ||||
|         return self.labels()[index]; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async function showDialog() { | ||||
|     glob.activeDialog = $dialog; | ||||
| 
 | ||||
|     await labelsModel.loadLabels(); | ||||
| 
 | ||||
|     $dialog.dialog({ | ||||
|         modal: true, | ||||
|         width: 800, | ||||
|         height: 500 | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| ko.applyBindings(labelsModel, $dialog[0]); | ||||
| 
 | ||||
| $dialog.on('focus', '.label-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 autocomplete.js
 | ||||
|             source: labelNames.map(label => { | ||||
|                 return { | ||||
|                     label: label, | ||||
|                     value: label | ||||
|                 } | ||||
|             }), | ||||
|             minLength: 0 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     $(this).autocomplete("search", $(this).val()); | ||||
| }); | ||||
| 
 | ||||
| $dialog.on('focus', '.label-value', async function (e) { | ||||
|     if (!$(this).hasClass("ui-autocomplete-input")) { | ||||
|         const labelName = $(this).parent().parent().find('.label-name').val(); | ||||
| 
 | ||||
|         if (labelName.trim() === "") { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const labelValues = await server.get('labels/values/' + encodeURIComponent(labelName)); | ||||
| 
 | ||||
|         if (labelValues.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: labelValues.map(label => { | ||||
|                 return { | ||||
|                     label: label, | ||||
|                     value: label | ||||
|                 } | ||||
|             }), | ||||
|             minLength: 0 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     $(this).autocomplete("search", $(this).val()); | ||||
| }); | ||||
| 
 | ||||
| export default { | ||||
|     showDialog | ||||
| }; | ||||
| @ -1,250 +0,0 @@ | ||||
| import noteDetailService from '../services/note_detail.js'; | ||||
| import server from '../services/server.js'; | ||||
| import infoService from "../services/info.js"; | ||||
| import linkService from "../services/link.js"; | ||||
| import treeUtils from "../services/tree_utils.js"; | ||||
| 
 | ||||
| const $dialog = $("#relations-dialog"); | ||||
| const $saveRelationsButton = $("#save-relations-button"); | ||||
| const $relationsBody = $('#relations-table tbody'); | ||||
| 
 | ||||
| const relationsModel = new RelationsModel(); | ||||
| let relationNames = []; | ||||
| 
 | ||||
| function RelationsModel() { | ||||
|     const self = this; | ||||
| 
 | ||||
|     this.relations = ko.observableArray(); | ||||
| 
 | ||||
|     this.updateRelationPositions = function() { | ||||
|         let position = 0; | ||||
| 
 | ||||
|         // we need to update positions by searching in the DOM, because order of the
 | ||||
|         // relations in the viewmodel (self.relations()) stays the same
 | ||||
|         $relationsBody.find('input[name="position"]').each(function() { | ||||
|             const relation = self.getTargetRelation(this); | ||||
| 
 | ||||
|             relation().position = position++; | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     async function showRelations(relations) { | ||||
|         for (const relation of relations) { | ||||
|             relation.targetNoteId = await treeUtils.getNoteTitle(relation.targetNoteId) + " (" + relation.targetNoteId + ")"; | ||||
|         } | ||||
| 
 | ||||
|         self.relations(relations.map(ko.observable)); | ||||
|     } | ||||
| 
 | ||||
|     this.loadRelations = async function() { | ||||
|         const noteId = noteDetailService.getCurrentNoteId(); | ||||
| 
 | ||||
|         const relations = await server.get('notes/' + noteId + '/relations'); | ||||
| 
 | ||||
|         await showRelations(relations); | ||||
| 
 | ||||
|         addLastEmptyRow(); | ||||
| 
 | ||||
|         relationNames = await server.get('relations/names'); | ||||
| 
 | ||||
|         // relation might not be rendered immediatelly so could not focus
 | ||||
|         setTimeout(() => $(".relation-name:last").focus(), 100); | ||||
| 
 | ||||
|         $relationsBody.sortable({ | ||||
|             handle: '.handle', | ||||
|             containment: $relationsBody, | ||||
|             update: this.updateRelationPositions | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     this.deleteRelation = function(data, event) { | ||||
|         const relation = self.getTargetRelation(event.target); | ||||
|         const relationData = relation(); | ||||
| 
 | ||||
|         if (relationData) { | ||||
|             relationData.isDeleted = true; | ||||
| 
 | ||||
|             relation(relationData); | ||||
| 
 | ||||
|             addLastEmptyRow(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     function isValid() { | ||||
|         for (let relations = self.relations(), i = 0; i < relations.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.
 | ||||
|         $saveRelationsButton.focus(); | ||||
| 
 | ||||
|         if (!isValid()) { | ||||
|             alert("Please fix all validation errors and try saving again."); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         self.updateRelationPositions(); | ||||
| 
 | ||||
|         const noteId = noteDetailService.getCurrentNoteId(); | ||||
| 
 | ||||
|         const relationsToSave = self.relations() | ||||
|             .map(relation => relation()) | ||||
|             .filter(relation => relation.relationId !== "" || relation.name !== ""); | ||||
| 
 | ||||
|         relationsToSave.forEach(relation => relation.targetNoteId = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel(relation.targetNoteId))); | ||||
| 
 | ||||
|         console.log(relationsToSave); | ||||
| 
 | ||||
|         const relations = await server.put('notes/' + noteId + '/relations', relationsToSave); | ||||
| 
 | ||||
|         await showRelations(relations); | ||||
| 
 | ||||
|         addLastEmptyRow(); | ||||
| 
 | ||||
|         infoService.showMessage("Relations have been saved."); | ||||
| 
 | ||||
|         noteDetailService.loadRelationList(); | ||||
|     }; | ||||
| 
 | ||||
|     function addLastEmptyRow() { | ||||
|         const relations = self.relations().filter(attr => !attr().isDeleted); | ||||
|         const last = relations.length === 0 ? null : relations[relations.length - 1](); | ||||
| 
 | ||||
|         if (!last || last.name.trim() !== "" || last.targetNoteId !== "") { | ||||
|             self.relations.push(ko.observable({ | ||||
|                 relationId: '', | ||||
|                 name: '', | ||||
|                 targetNoteId: '', | ||||
|                 isInheritable: false, | ||||
|                 isDeleted: false, | ||||
|                 position: 0 | ||||
|             })); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     this.relationChanged = function (data, event) { | ||||
|         addLastEmptyRow(); | ||||
| 
 | ||||
|         const relation = self.getTargetRelation(event.target); | ||||
| 
 | ||||
|         relation.valueHasMutated(); | ||||
|     }; | ||||
| 
 | ||||
|     this.isNotUnique = function(index) { | ||||
|         const cur = self.relations()[index](); | ||||
| 
 | ||||
|         if (cur.name.trim() === "") { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         for (let relations = self.relations(), i = 0; i < relations.length; i++) { | ||||
|             const relation = relations[i](); | ||||
| 
 | ||||
|             if (index !== i && cur.name === relation.name) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     }; | ||||
| 
 | ||||
|     this.isEmptyName = function(index) { | ||||
|         const cur = self.relations()[index](); | ||||
| 
 | ||||
|         return cur.name.trim() === "" && (cur.relationId !== "" || cur.targetNoteId !== ""); | ||||
|     }; | ||||
| 
 | ||||
|     this.getTargetRelation = function(target) { | ||||
|         const context = ko.contextFor(target); | ||||
|         const index = context.$index(); | ||||
| 
 | ||||
|         return self.relations()[index]; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async function showDialog() { | ||||
|     glob.activeDialog = $dialog; | ||||
| 
 | ||||
|     await relationsModel.loadRelations(); | ||||
| 
 | ||||
|     $dialog.dialog({ | ||||
|         modal: true, | ||||
|         width: 900, | ||||
|         height: 500 | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| ko.applyBindings(relationsModel, document.getElementById('relations-dialog')); | ||||
| 
 | ||||
| $dialog.on('focus', '.relation-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 autocomplete.js
 | ||||
|             source: relationNames.map(relation => { | ||||
|                 return { | ||||
|                     label: relation, | ||||
|                     value: relation | ||||
|                 } | ||||
|             }), | ||||
|             minLength: 0 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     $(this).autocomplete("search", $(this).val()); | ||||
| }); | ||||
| 
 | ||||
| async function initNoteAutocomplete($el) { | ||||
|     if (!$el.hasClass("ui-autocomplete-input")) { | ||||
|         await $el.autocomplete({ | ||||
|             source: async function (request, response) { | ||||
|                 const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term)); | ||||
| 
 | ||||
|                 if (result.length > 0) { | ||||
|                     response(result.map(row => { | ||||
|                         return { | ||||
|                             label: row.label, | ||||
|                             value: row.label + ' (' + row.value + ')' | ||||
|                         } | ||||
|                     })); | ||||
|                 } | ||||
|                 else { | ||||
|                     response([{ | ||||
|                         label: "No results", | ||||
|                         value: "No results" | ||||
|                     }]); | ||||
|                 } | ||||
|             }, | ||||
|             minLength: 0, | ||||
|             select: function (event, ui) { | ||||
|                 if (ui.item.value === 'No results') { | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| $dialog.on('focus', '.relation-target-note-id', async function () { | ||||
|     await initNoteAutocomplete($(this)); | ||||
| }); | ||||
| 
 | ||||
| $dialog.on('click', '.relations-show-recent-notes', async function () { | ||||
|     const $autocomplete = $(this).parent().find('.relation-target-note-id'); | ||||
| 
 | ||||
|     await initNoteAutocomplete($autocomplete); | ||||
| 
 | ||||
|     $autocomplete.autocomplete("search", ""); | ||||
| }); | ||||
| 
 | ||||
| export default { | ||||
|     showDialog | ||||
| }; | ||||
							
								
								
									
										1
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,5 @@ | ||||
| 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'; | ||||
|  | ||||
| @ -12,8 +12,6 @@ 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"; | ||||
| 
 | ||||
| function registerEntrypoints() { | ||||
| @ -42,12 +40,6 @@ function registerEntrypoints() { | ||||
|     $(".show-attributes-button").click(attributesDialog.showDialog); | ||||
|     utils.bindShortcut('alt+a', attributesDialog.showDialog); | ||||
| 
 | ||||
|     $(".show-labels-button").click(labelsDialog.showDialog); | ||||
|     utils.bindShortcut('alt+l', labelsDialog.showDialog); | ||||
| 
 | ||||
|     $(".show-relations-button").click(relationsDialog.showDialog); | ||||
|     utils.bindShortcut('alt+r', relationsDialog.showDialog); | ||||
| 
 | ||||
|     $("#options-button").click(optionsDialog.showDialog); | ||||
| 
 | ||||
|     utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog); | ||||
|  | ||||
| @ -29,10 +29,6 @@ const $noteDetailComponentWrapper = $("#note-detail-component-wrapper"); | ||||
| const $noteIdDisplay = $("#note-id-display"); | ||||
| const $attributeList = $("#attribute-list"); | ||||
| const $attributeListInner = $("#attribute-list-inner"); | ||||
| const $labelList = $("#label-list"); | ||||
| const $labelListInner = $("#label-list-inner"); | ||||
| const $relationList = $("#relation-list"); | ||||
| const $relationListInner = $("#relation-list-inner"); | ||||
| const $childrenOverview = $("#children-overview"); | ||||
| const $scriptArea = $("#note-detail-script-area"); | ||||
| const $promotedAttributesContainer = $("#note-detail-promoted-attributes"); | ||||
| @ -187,18 +183,14 @@ async function loadNoteDetail(noteId) { | ||||
|     // after loading new note make sure editor is scrolled to the top
 | ||||
|     $noteDetailWrapper.scrollTop(0); | ||||
| 
 | ||||
|     const labels = await loadLabelList(); | ||||
| 
 | ||||
|     const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview'); | ||||
|     await showChildrenOverview(hideChildrenOverview); | ||||
| 
 | ||||
|     await loadRelationList(); | ||||
| 
 | ||||
|     $scriptArea.html(''); | ||||
| 
 | ||||
|     await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView'); | ||||
| 
 | ||||
|     await loadAttributes(); | ||||
|     const attributes = await loadAttributes(); | ||||
| 
 | ||||
|     const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview'); | ||||
|     await showChildrenOverview(hideChildrenOverview); | ||||
| } | ||||
| 
 | ||||
| async function showChildrenOverview(hideChildrenOverview) { | ||||
| @ -411,50 +403,8 @@ async function loadAttributes() { | ||||
|             $attributeList.show(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async function loadLabelList() { | ||||
|     const noteId = getCurrentNoteId(); | ||||
| 
 | ||||
|     const labels = await server.get('notes/' + noteId + '/labels'); | ||||
| 
 | ||||
|     $labelListInner.html(''); | ||||
| 
 | ||||
|     if (labels.length > 0) { | ||||
|         for (const label of labels) { | ||||
|             $labelListInner.append(utils.formatLabel(label) + " "); | ||||
|         } | ||||
| 
 | ||||
|         $labelList.show(); | ||||
|     } | ||||
|     else { | ||||
|         $labelList.hide(); | ||||
|     } | ||||
| 
 | ||||
|     return labels; | ||||
| } | ||||
| 
 | ||||
| async function loadRelationList() { | ||||
|     const noteId = getCurrentNoteId(); | ||||
| 
 | ||||
|     const relations = await server.get('notes/' + noteId + '/relations'); | ||||
| 
 | ||||
|     $relationListInner.html(''); | ||||
| 
 | ||||
|     if (relations.length > 0) { | ||||
|         for (const relation of relations) { | ||||
|             $relationListInner.append(relation.name + " = "); | ||||
|             $relationListInner.append(await linkService.createNoteLink(relation.targetNoteId)); | ||||
|             $relationListInner.append(" "); | ||||
|         } | ||||
| 
 | ||||
|         $relationList.show(); | ||||
|     } | ||||
|     else { | ||||
|         $relationList.hide(); | ||||
|     } | ||||
| 
 | ||||
|     return relations; | ||||
|     return attributes; | ||||
| } | ||||
| 
 | ||||
| async function loadNote(noteId) { | ||||
| @ -535,8 +485,6 @@ export default { | ||||
|     newNoteCreated, | ||||
|     focus, | ||||
|     loadAttributes, | ||||
|     loadLabelList, | ||||
|     loadRelationList, | ||||
|     saveNote, | ||||
|     saveNoteIfChanged, | ||||
|     noteChanged | ||||
|  | ||||
| @ -6,56 +6,7 @@ const repository = require('../../services/repository'); | ||||
| const Attribute = require('../../entities/attribute'); | ||||
| 
 | ||||
| async function getEffectiveNoteAttributes(req) { | ||||
|     const noteId = req.params.noteId; | ||||
| 
 | ||||
|     const attributes = await repository.getEntities(` | ||||
|         WITH RECURSIVE tree(noteId, level) AS ( | ||||
|         SELECT ?, 0 | ||||
|             UNION | ||||
|             SELECT branches.parentNoteId, tree.level + 1 FROM branches | ||||
|             JOIN tree ON branches.noteId = tree.noteId | ||||
|             JOIN notes ON notes.noteId = branches.parentNoteId | ||||
|             WHERE notes.isDeleted = 0 AND branches.isDeleted = 0 | ||||
|         ) | ||||
|         SELECT attributes.* FROM attributes JOIN tree ON attributes.noteId = tree.noteId  | ||||
|         WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR attributes.noteId = ?) | ||||
|         ORDER BY level, noteId, position`, [noteId, noteId]);
 | ||||
|         // attributes are ordered so that "closest" attributes are first
 | ||||
|         // we order by noteId so that attributes from same note stay together. Actual noteId ordering doesn't matter.
 | ||||
| 
 | ||||
|     const filteredAttributes = attributes.filter((attr, index) => { | ||||
|         if (attr.isDefinition()) { | ||||
|             const firstDefinitionIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name); | ||||
| 
 | ||||
|             // keep only if this element is the first definition for this type & name
 | ||||
|             return firstDefinitionIndex === index; | ||||
|         } | ||||
|         else { | ||||
|             const definitionAttr = attributes.find(el => el.type === attr.type + '-definition' && el.name === attr.name); | ||||
| 
 | ||||
|             if (!definitionAttr) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             const definition = definitionAttr.value; | ||||
| 
 | ||||
|             if (definition.multiplicityType === 'multivalue') { | ||||
|                 return true; | ||||
|             } | ||||
|             else { | ||||
|                 const firstAttrIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name); | ||||
| 
 | ||||
|                 // in case of single-valued attribute we'll keep it only if it's first (closest)
 | ||||
|                 return firstAttrIndex === index; | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     for (const attr of filteredAttributes) { | ||||
|         attr.isOwned = attr.noteId === noteId; | ||||
|     } | ||||
| 
 | ||||
|     return filteredAttributes; | ||||
|     return await attributeService.getEffectiveAttributes(req.params.noteId); | ||||
| } | ||||
| 
 | ||||
| async function updateNoteAttribute(req) { | ||||
| @ -136,7 +87,7 @@ async function updateNoteAttributes(req) { | ||||
|         await attributeEntity.save(); | ||||
|     } | ||||
| 
 | ||||
|     return await getEffectiveNoteAttributes(req); | ||||
|     return await attributeService.getEffectiveAttributes(noteId); | ||||
| } | ||||
| 
 | ||||
| async function getAttributeNames(req) { | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
| 
 | ||||
| const labelService = require('../../services/labels'); | ||||
| const scriptService = require('../../services/script'); | ||||
| const relationService = require('../../services/relations'); | ||||
| const attributeService = require('../../services/attributes'); | ||||
| const repository = require('../../services/repository'); | ||||
| 
 | ||||
| async function exec(req) { | ||||
| @ -40,9 +40,9 @@ async function getRelationBundles(req) { | ||||
|     const noteId = req.params.noteId; | ||||
|     const relationName = req.params.relationName; | ||||
| 
 | ||||
|     const relations = await relationService.getEffectiveRelations(noteId); | ||||
|     const filtered = relations.filter(relation => relation.name === relationName); | ||||
|     const targetNoteIds = filtered.map(relation => relation.targetNoteId); | ||||
|     const attributes = await attributeService.getEffectiveAttributes(noteId); | ||||
|     const filtered = attributes.filter(attr => attr.type === 'relation' && attr.name === relationName); | ||||
|     const targetNoteIds = filtered.map(relation => relation.value); | ||||
|     const uniqueNoteIds = Array.from(new Set(targetNoteIds)); | ||||
| 
 | ||||
|     const bundles = []; | ||||
|  | ||||
| @ -26,8 +26,6 @@ const anonymizationRoute = require('./api/anonymization'); | ||||
| const cleanupRoute = require('./api/cleanup'); | ||||
| const imageRoute = require('./api/image'); | ||||
| const attributesRoute = require('./api/attributes'); | ||||
| const labelsRoute = require('./api/labels'); | ||||
| const relationsRoute = require('./api/relations'); | ||||
| const scriptRoute = require('./api/script'); | ||||
| const senderRoute = require('./api/sender'); | ||||
| const filesRoute = require('./api/file_upload'); | ||||
| @ -141,15 +139,6 @@ function register(app) { | ||||
|     apiRoute(GET, '/api/attributes/names', attributesRoute.getAttributeNames); | ||||
|     apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); | ||||
| 
 | ||||
|     apiRoute(GET, '/api/notes/:noteId/labels', labelsRoute.getNoteLabels); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/labels', labelsRoute.updateNoteLabels); | ||||
|     apiRoute(GET, '/api/labels/names', labelsRoute.getAllLabelNames); | ||||
|     apiRoute(GET, '/api/labels/values/:labelName', labelsRoute.getValuesForLabel); | ||||
| 
 | ||||
|     apiRoute(GET, '/api/notes/:noteId/relations', relationsRoute.getNoteRelations); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/relations', relationsRoute.updateNoteRelations); | ||||
|     apiRoute(GET, '/api/relations/names', relationsRoute.getAllRelationNames); | ||||
| 
 | ||||
|     route(GET, '/api/images/:imageId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage); | ||||
|     route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddleware], imageRoute.uploadImage, apiResultHandler); | ||||
| 
 | ||||
|  | ||||
| @ -22,7 +22,7 @@ const BUILTIN_ATTRIBUTES = [ | ||||
|     { type: 'relation', name: 'runOnNoteTitleChange' } | ||||
| ]; | ||||
| 
 | ||||
| async function getNotesWithAttribute(name, value) { | ||||
| async function getNotesWithLabel(name, value) { | ||||
|     let notes; | ||||
| 
 | ||||
|     if (value !== undefined) { | ||||
| @ -37,8 +37,8 @@ async function getNotesWithAttribute(name, value) { | ||||
|     return notes; | ||||
| } | ||||
| 
 | ||||
| async function getNoteWithAttribute(name, value) { | ||||
|     const notes = await getNotesWithAttribute(name, value); | ||||
| async function getNoteWithLabel(name, value) { | ||||
|     const notes = await getNotesWithLabel(name, value); | ||||
| 
 | ||||
|     return notes.length > 0 ? notes[0] : null; | ||||
| } | ||||
| @ -70,10 +70,62 @@ async function getAttributeNames(type, nameLike) { | ||||
|     return names; | ||||
| } | ||||
| 
 | ||||
| async function getEffectiveAttributes(noteId) { | ||||
|     const attributes = await repository.getEntities(` | ||||
|         WITH RECURSIVE tree(noteId, level) AS ( | ||||
|         SELECT ?, 0 | ||||
|             UNION | ||||
|             SELECT branches.parentNoteId, tree.level + 1 FROM branches | ||||
|             JOIN tree ON branches.noteId = tree.noteId | ||||
|             JOIN notes ON notes.noteId = branches.parentNoteId | ||||
|             WHERE notes.isDeleted = 0 AND branches.isDeleted = 0 | ||||
|         ) | ||||
|         SELECT attributes.* FROM attributes JOIN tree ON attributes.noteId = tree.noteId  | ||||
|         WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR attributes.noteId = ?) | ||||
|         ORDER BY level, noteId, position`, [noteId, noteId]);
 | ||||
|     // attributes are ordered so that "closest" attributes are first
 | ||||
|     // we order by noteId so that attributes from same note stay together. Actual noteId ordering doesn't matter.
 | ||||
| 
 | ||||
|     const filteredAttributes = attributes.filter((attr, index) => { | ||||
|         if (attr.isDefinition()) { | ||||
|             const firstDefinitionIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name); | ||||
| 
 | ||||
|             // keep only if this element is the first definition for this type & name
 | ||||
|             return firstDefinitionIndex === index; | ||||
|         } | ||||
|         else { | ||||
|             const definitionAttr = attributes.find(el => el.type === attr.type + '-definition' && el.name === attr.name); | ||||
| 
 | ||||
|             if (!definitionAttr) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             const definition = definitionAttr.value; | ||||
| 
 | ||||
|             if (definition.multiplicityType === 'multivalue') { | ||||
|                 return true; | ||||
|             } | ||||
|             else { | ||||
|                 const firstAttrIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name); | ||||
| 
 | ||||
|                 // in case of single-valued attribute we'll keep it only if it's first (closest)
 | ||||
|                 return firstAttrIndex === index; | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     for (const attr of filteredAttributes) { | ||||
|         attr.isOwned = attr.noteId === noteId; | ||||
|     } | ||||
| 
 | ||||
|     return filteredAttributes; | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|     getNotesWithAttribute, | ||||
|     getNoteWithAttribute, | ||||
|     getNotesWithLabel, | ||||
|     getNoteWithLabel, | ||||
|     createAttribute, | ||||
|     getAttributeNames, | ||||
|     getEffectiveAttributes, | ||||
|     BUILTIN_ATTRIBUTES | ||||
| }; | ||||
| @ -3,7 +3,7 @@ const noteService = require('./notes'); | ||||
| const sql = require('./sql'); | ||||
| const utils = require('./utils'); | ||||
| const dateUtils = require('./date_utils'); | ||||
| const labelService = require('./labels'); | ||||
| const attributeService = require('./attributes'); | ||||
| const dateNoteService = require('./date_notes'); | ||||
| const treeService = require('./tree'); | ||||
| const config = require('./config'); | ||||
| @ -45,15 +45,14 @@ function ScriptApi(startNote, currentNote, workNote) { | ||||
| 
 | ||||
|     this.getNote = repository.getNote; | ||||
|     this.getBranch = repository.getBranch; | ||||
|     this.getLabel = repository.getLabel; | ||||
|     this.getRelation = repository.getRelation; | ||||
|     this.getAttribute = repository.getAttribute; | ||||
|     this.getImage = repository.getImage; | ||||
|     this.getEntity = repository.getEntity; | ||||
|     this.getEntities = repository.getEntities; | ||||
| 
 | ||||
|     this.createLabel = labelService.createLabel; | ||||
|     this.getNotesWithLabel = labelService.getNotesWithLabel; | ||||
|     this.getNoteWithLabel = labelService.getNoteWithLabel; | ||||
|     this.createAttribute = attributeService.createAttribute; | ||||
|     this.getNotesWithLabel = attributeService.getNotesWithLabel; | ||||
|     this.getNoteWithLabel = attributeService.getNoteWithLabel; | ||||
| 
 | ||||
|     this.createNote = noteService.createNote; | ||||
| 
 | ||||
|  | ||||
| @ -168,10 +168,8 @@ | ||||
|                 <span class="caret"></span> | ||||
|               </button> | ||||
|               <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">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> | ||||
|                 <li><a id="upload-file-button">Upload file</a></li> | ||||
|               </ul> | ||||
| @ -264,20 +262,6 @@ | ||||
| 
 | ||||
|           <span id="attribute-list-inner"></span> | ||||
|         </div> | ||||
| 
 | ||||
|         <div id="labels-and-relations" style="display: none;"> | ||||
|           <span id="label-list"> | ||||
|             <button class="btn btn-sm show-labels-button">Labels:</button> | ||||
| 
 | ||||
|             <span id="label-list-inner"></span> | ||||
|           </span> | ||||
| 
 | ||||
|           <span id="relation-list"> | ||||
|             <button class="btn btn-sm show-relations-button">Relations:</button> | ||||
| 
 | ||||
|             <span id="relation-list-inner"></span> | ||||
|           </span> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
| @ -667,105 +651,6 @@ | ||||
|       </form> | ||||
|     </div> | ||||
| 
 | ||||
|     <div id="labels-dialog" title="Note labels" 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-labels-button" type="submit">Save changes <kbd>enter</kbd></button> | ||||
|       </div> | ||||
| 
 | ||||
|       <div style="height: 97%; overflow: auto"> | ||||
|         <table id="labels-table" class="table"> | ||||
|           <thead> | ||||
|             <tr> | ||||
|               <th></th> | ||||
|               <th>ID</th> | ||||
|               <th>Name</th> | ||||
|               <th>Value</th> | ||||
|               <th></th> | ||||
|             </tr> | ||||
|           </thead> | ||||
|           <tbody data-bind="foreach: labels"> | ||||
|             <tr data-bind="if: !isDeleted"> | ||||
|               <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: labelId" style="width: 150px;"></td> | ||||
|               <td> | ||||
|                 <!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event --> | ||||
|                 <input type="text" class="label-name form-control" data-bind="value: name, valueUpdate: 'blur',  event: { blur: $parent.labelChanged }"/> | ||||
|                 <div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate label.</div> | ||||
|                 <div style="color: red" data-bind="if: $parent.isEmptyName($index())">Label name can't be empty.</div> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <input type="text" class="label-value form-control" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.labelChanged }" style="width: 300px"/> | ||||
|               </td> | ||||
|               <td title="Delete" style="padding: 13px; cursor: pointer;"> | ||||
|                 <span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteLabel"></span> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|       </form> | ||||
|     </div> | ||||
| 
 | ||||
|     <div id="relations-dialog" title="Note relations" 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-relations-button" type="submit">Save changes <kbd>enter</kbd></button> | ||||
|         </div> | ||||
| 
 | ||||
|         <div style="height: 97%; overflow: auto"> | ||||
|           <table id="relations-table" class="table"> | ||||
|             <thead> | ||||
|             <tr> | ||||
|               <th></th> | ||||
|               <th>ID</th> | ||||
|               <th>Relation name</th> | ||||
|               <th>Target note</th> | ||||
|               <th>Inheritable</th> | ||||
|               <th></th> | ||||
|             </tr> | ||||
|             </thead> | ||||
|             <tbody data-bind="foreach: relations"> | ||||
|             <tr data-bind="if: !isDeleted"> | ||||
|               <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: relationId" style="width: 150px;"></td> | ||||
|               <td> | ||||
|                 <!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event --> | ||||
|                 <input type="text" class="relation-name form-control" data-bind="value: name, valueUpdate: 'blur',  event: { blur: $parent.relationChanged }"/> | ||||
|                 <div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate relation.</div> | ||||
|                 <div style="color: red" data-bind="if: $parent.isEmptyName($index())">Relation name can't be empty.</div> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <div class="input-group"> | ||||
|                   <input class="form-control relation-target-note-id" | ||||
|                          placeholder="search for note by its name" | ||||
|                          data-bind="value: targetNoteId, valueUpdate: 'blur', event: { blur: $parent.relationChanged }" | ||||
|                          style="width: 300px;"> | ||||
| 
 | ||||
|                   <span class="input-group-addon relations-show-recent-notes" title="Show recent notes" style="background: url('/images/icons/clock-16.png') no-repeat center; cursor: pointer;"></span> | ||||
|                 </div> | ||||
|               </td> | ||||
|               <td title="Inheritable relations are automatically inherited to the child notes"> | ||||
|                 <input type="checkbox" value="1" data-bind="checked: isInheritable" /> | ||||
|               </td> | ||||
|               <td title="Delete" style="padding: 13px; cursor: pointer;"> | ||||
|                 <span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteRelation"></span> | ||||
|               </td> | ||||
|             </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       </form> | ||||
|     </div> | ||||
| 
 | ||||
|     <div id="tooltip" style="display: none;"></div> | ||||
| 
 | ||||
|     <script type="text/javascript"> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 azivner
						azivner