mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	refactored attributes out of note detail, fixes #213
This commit is contained in:
		| @@ -2,7 +2,7 @@ import noteDetailService from '../services/note_detail.js'; | ||||
| import server from '../services/server.js'; | ||||
| import infoService from "../services/info.js"; | ||||
| import treeUtils from "../services/tree_utils.js"; | ||||
| import linkService from "../services/link.js"; | ||||
| import attributeService from "../services/attributes.js"; | ||||
|  | ||||
| const $dialog = $("#attributes-dialog"); | ||||
| const $saveAttributesButton = $("#save-attributes-button"); | ||||
| @@ -165,7 +165,7 @@ function AttributesModel() { | ||||
|  | ||||
|         infoService.showMessage("Attributes have been saved."); | ||||
|  | ||||
|         noteDetailService.refreshAttributes(); | ||||
|         attributeService.refreshAttributes(); | ||||
|     }; | ||||
|  | ||||
|     function addLastEmptyRow() { | ||||
|   | ||||
							
								
								
									
										298
									
								
								src/public/javascripts/services/attributes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								src/public/javascripts/services/attributes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,298 @@ | ||||
| import server from "./server.js"; | ||||
| import utils from "./utils.js"; | ||||
| import messagingService from "./messaging.js"; | ||||
| import treeUtils from "./tree_utils.js"; | ||||
| import noteAutocompleteService from "./note_autocomplete.js"; | ||||
| import treeService from "./tree.js"; | ||||
| import linkService from "./link.js"; | ||||
| import infoService from "./info.js"; | ||||
| import noteDetailService from "./note_detail.js"; | ||||
|  | ||||
| const $attributeList = $("#attribute-list"); | ||||
| const $attributeListInner = $("#attribute-list-inner"); | ||||
| const $promotedAttributesContainer = $("#note-detail-promoted-attributes"); | ||||
|  | ||||
| let attributePromise; | ||||
|  | ||||
| async function refreshAttributes() { | ||||
|     attributePromise = server.get('notes/' + noteDetailService.getCurrentNoteId() + '/attributes'); | ||||
|  | ||||
|     await showAttributes(); | ||||
| } | ||||
|  | ||||
| async function getAttributes() { | ||||
|     return await attributePromise; | ||||
| } | ||||
|  | ||||
| async function showAttributes() { | ||||
|     $promotedAttributesContainer.empty(); | ||||
|     $attributeList.hide(); | ||||
|  | ||||
|     const noteId = noteDetailService.getCurrentNoteId(); | ||||
|  | ||||
|     const attributes = await attributePromise; | ||||
|  | ||||
|     const promoted = attributes.filter(attr => | ||||
|         (attr.type === 'label-definition' || attr.type === 'relation-definition') | ||||
|         && !attr.name.startsWith("child:") | ||||
|         && attr.value.isPromoted); | ||||
|  | ||||
|     let idx = 1; | ||||
|  | ||||
|     async function createRow(definitionAttr, valueAttr) { | ||||
|         const definition = definitionAttr.value; | ||||
|         const inputId = "promoted-input-" + idx; | ||||
|         const $tr = $("<tr>"); | ||||
|         const $labelCell = $("<th>").append(valueAttr.name); | ||||
|         const $input = $("<input>") | ||||
|             .prop("id", inputId) | ||||
|             .prop("tabindex", definitionAttr.position) | ||||
|             .prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one | ||||
|             .prop("attribute-type", valueAttr.type) | ||||
|             .prop("attribute-name", valueAttr.name) | ||||
|             .prop("value", valueAttr.value) | ||||
|             .addClass("form-control") | ||||
|             .addClass("promoted-attribute-input") | ||||
|             .change(promotedAttributeChanged); | ||||
|  | ||||
|         idx++; | ||||
|  | ||||
|         const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input)); | ||||
|  | ||||
|         const $actionCell = $("<td>"); | ||||
|         const $multiplicityCell = $("<td>"); | ||||
|  | ||||
|         $tr | ||||
|             .append($labelCell) | ||||
|             .append($inputCell) | ||||
|             .append($actionCell) | ||||
|             .append($multiplicityCell); | ||||
|  | ||||
|         if (valueAttr.type === 'label') { | ||||
|             if (definition.labelType === 'text') { | ||||
|                 $input.prop("type", "text"); | ||||
|  | ||||
|                 // no need to await for this, can be done asynchronously | ||||
|                 server.get('attributes/values/' + encodeURIComponent(valueAttr.name)).then(attributeValues => { | ||||
|                     if (attributeValues.length === 0) { | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     attributeValues = attributeValues.map(attribute => { return { value: attribute }; }); | ||||
|  | ||||
|                     $input.autocomplete({ | ||||
|                         appendTo: document.querySelector('body'), | ||||
|                         hint: false, | ||||
|                         autoselect: true, | ||||
|                         openOnFocus: true, | ||||
|                         minLength: 0 | ||||
|                     }, [{ | ||||
|                         displayKey: 'value', | ||||
|                         source: function (term, cb) { | ||||
|                             term = term.toLowerCase(); | ||||
|  | ||||
|                             const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term)); | ||||
|  | ||||
|                             cb(filtered); | ||||
|                         } | ||||
|                     }]); | ||||
|                 }); | ||||
|             } | ||||
|             else if (definition.labelType === 'number') { | ||||
|                 $input.prop("type", "number"); | ||||
|             } | ||||
|             else if (definition.labelType === 'boolean') { | ||||
|                 $input.prop("type", "checkbox"); | ||||
|  | ||||
|                 if (valueAttr.value === "true") { | ||||
|                     $input.prop("checked", "checked"); | ||||
|                 } | ||||
|             } | ||||
|             else if (definition.labelType === 'date') { | ||||
|                 $input.prop("type", "date"); | ||||
|  | ||||
|                 const $todayButton = $("<button>").addClass("btn btn-sm").text("Today").click(() => { | ||||
|                     $input.val(utils.formatDateISO(new Date())); | ||||
|                     $input.trigger("change"); | ||||
|                 }); | ||||
|  | ||||
|                 $actionCell.append($todayButton); | ||||
|             } | ||||
|             else if (definition.labelType === 'url') { | ||||
|                 $input.prop("placeholder", "http://website..."); | ||||
|  | ||||
|                 const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => { | ||||
|                     window.open($input.val(), '_blank'); | ||||
|                 }); | ||||
|  | ||||
|                 $actionCell.append($openButton); | ||||
|             } | ||||
|             else { | ||||
|                 messagingService.logError("Unknown labelType=" + definitionAttr.labelType); | ||||
|             } | ||||
|         } | ||||
|         else if (valueAttr.type === 'relation') { | ||||
|             if (valueAttr.value) { | ||||
|                 $input.val(await treeUtils.getNoteTitle(valueAttr.value)); | ||||
|             } | ||||
|  | ||||
|             // no need to wait for this | ||||
|             noteAutocompleteService.initNoteAutocomplete($input); | ||||
|  | ||||
|             $input.on('autocomplete:selected', function(event, suggestion, dataset) { | ||||
|                 promotedAttributeChanged(event); | ||||
|             }); | ||||
|  | ||||
|             $input.prop("data-selected-path", valueAttr.value); | ||||
|  | ||||
|             // ideally we'd use link instead of button which would allow tooltip preview, but | ||||
|             // we can't guarantee updating the link in the a element | ||||
|             const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => { | ||||
|                 const notePath = $input.prop("data-selected-path"); | ||||
|  | ||||
|                 treeService.activateNote(notePath); | ||||
|             }); | ||||
|  | ||||
|             $actionCell.append($openButton); | ||||
|         } | ||||
|         else { | ||||
|             messagingService.logError("Unknown attribute type=" + valueAttr.type); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (definition.multiplicityType === "multivalue") { | ||||
|             const addButton = $("<span>") | ||||
|                 .addClass("glyphicon glyphicon-plus pointer") | ||||
|                 .prop("title", "Add new attribute") | ||||
|                 .click(async () => { | ||||
|                     const $new = await createRow(definitionAttr, { | ||||
|                         attributeId: "", | ||||
|                         type: valueAttr.type, | ||||
|                         name: definitionAttr.name, | ||||
|                         value: "" | ||||
|                     }); | ||||
|  | ||||
|                     $tr.after($new); | ||||
|  | ||||
|                     $new.find('input').focus(); | ||||
|                 }); | ||||
|  | ||||
|             const removeButton = $("<span>") | ||||
|                 .addClass("glyphicon glyphicon-trash pointer") | ||||
|                 .prop("title", "Remove this attribute") | ||||
|                 .click(async () => { | ||||
|                     if (valueAttr.attributeId) { | ||||
|                         await server.remove("notes/" + noteId + "/attributes/" + valueAttr.attributeId); | ||||
|                     } | ||||
|  | ||||
|                     $tr.remove(); | ||||
|                 }); | ||||
|  | ||||
|             $multiplicityCell.append(addButton).append("   ").append(removeButton); | ||||
|         } | ||||
|  | ||||
|         return $tr; | ||||
|     } | ||||
|  | ||||
|     if (promoted.length > 0) { | ||||
|         const $tbody = $("<tbody>"); | ||||
|  | ||||
|         for (const definitionAttr of promoted) { | ||||
|             const definitionType = definitionAttr.type; | ||||
|             const valueType = definitionType.substr(0, definitionType.length - 11); | ||||
|  | ||||
|             let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType); | ||||
|  | ||||
|             if (valueAttrs.length === 0) { | ||||
|                 valueAttrs.push({ | ||||
|                     attributeId: "", | ||||
|                     type: valueType, | ||||
|                     name: definitionAttr.name, | ||||
|                     value: "" | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if (definitionAttr.value.multiplicityType === 'singlevalue') { | ||||
|                 valueAttrs = valueAttrs.slice(0, 1); | ||||
|             } | ||||
|  | ||||
|             for (const valueAttr of valueAttrs) { | ||||
|                 const $tr = await createRow(definitionAttr, valueAttr); | ||||
|  | ||||
|                 $tbody.append($tr); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // we replace the whole content in one step so there can't be any race conditions | ||||
|         // (previously we saw promoted attributes doubling) | ||||
|         $promotedAttributesContainer.empty().append($tbody); | ||||
|     } | ||||
|     else { | ||||
|         $attributeListInner.empty(); | ||||
|  | ||||
|         if (attributes.length > 0) { | ||||
|             for (const attribute of attributes) { | ||||
|                 if (attribute.type === 'label') { | ||||
|                     $attributeListInner.append(utils.formatLabel(attribute) + " "); | ||||
|                 } | ||||
|                 else if (attribute.type === 'relation') { | ||||
|                     if (attribute.value) { | ||||
|                         $attributeListInner.append('@' + attribute.name + "="); | ||||
|                         $attributeListInner.append(await linkService.createNoteLink(attribute.value)); | ||||
|                         $attributeListInner.append(" "); | ||||
|                     } | ||||
|                     else { | ||||
|                         messagingService.logError(`Relation ${attribute.attributeId} has empty target`); | ||||
|                     } | ||||
|                 } | ||||
|                 else if (attribute.type === 'label-definition' || attribute.type === 'relation-definition') { | ||||
|                     $attributeListInner.append(attribute.name + " definition "); | ||||
|                 } | ||||
|                 else { | ||||
|                     messagingService.logError("Unknown attr type: " + attribute.type); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             $attributeList.show(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return attributes; | ||||
| } | ||||
|  | ||||
| async function promotedAttributeChanged(event) { | ||||
|     const $attr = $(event.target); | ||||
|  | ||||
|     let value; | ||||
|  | ||||
|     if ($attr.prop("type") === "checkbox") { | ||||
|         value = $attr.is(':checked') ? "true" : "false"; | ||||
|     } | ||||
|     else if ($attr.prop("attribute-type") === "relation") { | ||||
|         const selectedPath = $attr.prop("data-selected-path"); | ||||
|  | ||||
|         if (selectedPath) { | ||||
|             value = treeUtils.getNoteIdFromNotePath(selectedPath); | ||||
|         } | ||||
|     } | ||||
|     else { | ||||
|         value = $attr.val(); | ||||
|     } | ||||
|  | ||||
|     const result = await server.put("notes/" + noteDetailService.getCurrentNoteId() + "/attribute", { | ||||
|         attributeId: $attr.prop("attribute-id"), | ||||
|         type: $attr.prop("attribute-type"), | ||||
|         name: $attr.prop("attribute-name"), | ||||
|         value: value | ||||
|     }); | ||||
|  | ||||
|     $attr.prop("attribute-id", result.attributeId); | ||||
|  | ||||
|     infoService.showMessage("Attribute has been saved."); | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     getAttributes, | ||||
|     showAttributes, | ||||
|     refreshAttributes | ||||
| } | ||||
| @@ -3,11 +3,9 @@ import treeUtils from './tree_utils.js'; | ||||
| import noteTypeService from './note_type.js'; | ||||
| import protectedSessionService from './protected_session.js'; | ||||
| import protectedSessionHolder from './protected_session_holder.js'; | ||||
| import utils from './utils.js'; | ||||
| import server from './server.js'; | ||||
| import messagingService from "./messaging.js"; | ||||
| import infoService from "./info.js"; | ||||
| import linkService from "./link.js"; | ||||
| import treeCache from "./tree_cache.js"; | ||||
| import NoteFull from "../entities/note_full.js"; | ||||
| import noteDetailCode from './note_detail_code.js'; | ||||
| @@ -18,7 +16,7 @@ import noteDetailSearch from './note_detail_search.js'; | ||||
| import noteDetailRender from './note_detail_render.js'; | ||||
| import noteDetailRelationMap from './note_detail_relation_map.js'; | ||||
| import bundleService from "./bundle.js"; | ||||
| import noteAutocompleteService from "./note_autocomplete.js"; | ||||
| import attributeService from "./attributes.js"; | ||||
|  | ||||
| const $noteTitle = $("#note-title"); | ||||
|  | ||||
| @@ -28,11 +26,8 @@ const $protectButton = $("#protect-button"); | ||||
| const $unprotectButton = $("#unprotect-button"); | ||||
| const $noteDetailWrapper = $("#note-detail-wrapper"); | ||||
| const $noteIdDisplay = $("#note-id-display"); | ||||
| const $attributeList = $("#attribute-list"); | ||||
| const $attributeListInner = $("#attribute-list-inner"); | ||||
| const $childrenOverview = $("#children-overview"); | ||||
| const $scriptArea = $("#note-detail-script-area"); | ||||
| const $promotedAttributesContainer = $("#note-detail-promoted-attributes"); | ||||
|  | ||||
| let currentNote = null; | ||||
|  | ||||
| @@ -40,8 +35,6 @@ let noteChangeDisabled = false; | ||||
|  | ||||
| let isNoteChanged = false; | ||||
|  | ||||
| let attributePromise; | ||||
|  | ||||
| const components = { | ||||
|     'code': noteDetailCode, | ||||
|     'text': noteDetailText, | ||||
| @@ -181,7 +174,7 @@ async function loadNoteDetail(noteId) { | ||||
|     currentNote = loadedNote; | ||||
|  | ||||
|     // needs to happend after loading the note itself because it references current noteId | ||||
|     refreshAttributes(); | ||||
|     attributeService.refreshAttributes(); | ||||
|  | ||||
|     if (isNewNoteCreated) { | ||||
|         isNewNoteCreated = false; | ||||
| @@ -232,14 +225,14 @@ async function loadNoteDetail(noteId) { | ||||
|  | ||||
|     await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView'); | ||||
|  | ||||
|     await showAttributes(); | ||||
|     await attributeService.showAttributes(); | ||||
|  | ||||
|     await showChildrenOverview(); | ||||
| } | ||||
|  | ||||
| async function showChildrenOverview() { | ||||
|     const note = getCurrentNote(); | ||||
|     const attributes = await attributePromise; | ||||
|     const attributes = await attributeService.getAttributes(); | ||||
|     const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview') | ||||
|         || note.type === 'relation-map' | ||||
|         || note.type === 'image' | ||||
| @@ -267,283 +260,6 @@ async function showChildrenOverview() { | ||||
|     $childrenOverview.show(); | ||||
| } | ||||
|  | ||||
| async function refreshAttributes() { | ||||
|     attributePromise = server.get('notes/' + getCurrentNoteId() + '/attributes'); | ||||
|  | ||||
|     await showAttributes(); | ||||
| } | ||||
|  | ||||
| async function getAttributes() { | ||||
|     return await attributePromise; | ||||
| } | ||||
|  | ||||
| async function showAttributes() { | ||||
|     $promotedAttributesContainer.empty(); | ||||
|     $attributeList.hide(); | ||||
|  | ||||
|     const noteId = getCurrentNoteId(); | ||||
|  | ||||
|     const attributes = await attributePromise; | ||||
|  | ||||
|     const promoted = attributes.filter(attr => | ||||
|         (attr.type === 'label-definition' || attr.type === 'relation-definition') | ||||
|         && !attr.name.startsWith("child:") | ||||
|         && attr.value.isPromoted); | ||||
|  | ||||
|     let idx = 1; | ||||
|  | ||||
|     async function createRow(definitionAttr, valueAttr) { | ||||
|         const definition = definitionAttr.value; | ||||
|         const inputId = "promoted-input-" + idx; | ||||
|         const $tr = $("<tr>"); | ||||
|         const $labelCell = $("<th>").append(valueAttr.name); | ||||
|         const $input = $("<input>") | ||||
|             .prop("id", inputId) | ||||
|             .prop("tabindex", definitionAttr.position) | ||||
|             .prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one | ||||
|             .prop("attribute-type", valueAttr.type) | ||||
|             .prop("attribute-name", valueAttr.name) | ||||
|             .prop("value", valueAttr.value) | ||||
|             .addClass("form-control") | ||||
|             .addClass("promoted-attribute-input") | ||||
|             .change(promotedAttributeChanged); | ||||
|  | ||||
|         idx++; | ||||
|  | ||||
|         const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input)); | ||||
|  | ||||
|         const $actionCell = $("<td>"); | ||||
|         const $multiplicityCell = $("<td>"); | ||||
|  | ||||
|         $tr | ||||
|             .append($labelCell) | ||||
|             .append($inputCell) | ||||
|             .append($actionCell) | ||||
|             .append($multiplicityCell); | ||||
|  | ||||
|         if (valueAttr.type === 'label') { | ||||
|             if (definition.labelType === 'text') { | ||||
|                 $input.prop("type", "text"); | ||||
|  | ||||
|                 // no need to await for this, can be done asynchronously | ||||
|                 server.get('attributes/values/' + encodeURIComponent(valueAttr.name)).then(attributeValues => { | ||||
|                     if (attributeValues.length === 0) { | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     attributeValues = attributeValues.map(attribute => { return { value: attribute }; }); | ||||
|  | ||||
|                     $input.autocomplete({ | ||||
|                         appendTo: document.querySelector('body'), | ||||
|                         hint: false, | ||||
|                         autoselect: true, | ||||
|                         openOnFocus: true, | ||||
|                         minLength: 0 | ||||
|                     }, [{ | ||||
|                         displayKey: 'value', | ||||
|                         source: function (term, cb) { | ||||
|                             term = term.toLowerCase(); | ||||
|  | ||||
|                             const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term)); | ||||
|  | ||||
|                             cb(filtered); | ||||
|                         } | ||||
|                     }]); | ||||
|                 }); | ||||
|             } | ||||
|             else if (definition.labelType === 'number') { | ||||
|                 $input.prop("type", "number"); | ||||
|             } | ||||
|             else if (definition.labelType === 'boolean') { | ||||
|                 $input.prop("type", "checkbox"); | ||||
|  | ||||
|                 if (valueAttr.value === "true") { | ||||
|                     $input.prop("checked", "checked"); | ||||
|                 } | ||||
|             } | ||||
|             else if (definition.labelType === 'date') { | ||||
|                 $input.prop("type", "date"); | ||||
|  | ||||
|                 const $todayButton = $("<button>").addClass("btn btn-sm").text("Today").click(() => { | ||||
|                     $input.val(utils.formatDateISO(new Date())); | ||||
|                     $input.trigger("change"); | ||||
|                 }); | ||||
|  | ||||
|                 $actionCell.append($todayButton); | ||||
|             } | ||||
|             else if (definition.labelType === 'url') { | ||||
|                 $input.prop("placeholder", "http://website..."); | ||||
|  | ||||
|                 const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => { | ||||
|                     window.open($input.val(), '_blank'); | ||||
|                 }); | ||||
|  | ||||
|                 $actionCell.append($openButton); | ||||
|             } | ||||
|             else { | ||||
|                 messagingService.logError("Unknown labelType=" + definitionAttr.labelType); | ||||
|             } | ||||
|         } | ||||
|         else if (valueAttr.type === 'relation') { | ||||
|             if (valueAttr.value) { | ||||
|                 $input.val(await treeUtils.getNoteTitle(valueAttr.value)); | ||||
|             } | ||||
|  | ||||
|             // no need to wait for this | ||||
|             noteAutocompleteService.initNoteAutocomplete($input); | ||||
|  | ||||
|             $input.on('autocomplete:selected', function(event, suggestion, dataset) { | ||||
|                 promotedAttributeChanged(event); | ||||
|             }); | ||||
|  | ||||
|             $input.prop("data-selected-path", valueAttr.value); | ||||
|  | ||||
|             // ideally we'd use link instead of button which would allow tooltip preview, but | ||||
|             // we can't guarantee updating the link in the a element | ||||
|             const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => { | ||||
|                 const notePath = $input.prop("data-selected-path"); | ||||
|  | ||||
|                 treeService.activateNote(notePath); | ||||
|             }); | ||||
|  | ||||
|             $actionCell.append($openButton); | ||||
|         } | ||||
|         else { | ||||
|             messagingService.logError("Unknown attribute type=" + valueAttr.type); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (definition.multiplicityType === "multivalue") { | ||||
|             const addButton = $("<span>") | ||||
|                 .addClass("glyphicon glyphicon-plus pointer") | ||||
|                 .prop("title", "Add new attribute") | ||||
|                 .click(async () => { | ||||
|                 const $new = await createRow(definitionAttr, { | ||||
|                     attributeId: "", | ||||
|                     type: valueAttr.type, | ||||
|                     name: definitionAttr.name, | ||||
|                     value: "" | ||||
|                 }); | ||||
|  | ||||
|                 $tr.after($new); | ||||
|  | ||||
|                 $new.find('input').focus(); | ||||
|             }); | ||||
|  | ||||
|             const removeButton = $("<span>") | ||||
|                 .addClass("glyphicon glyphicon-trash pointer") | ||||
|                 .prop("title", "Remove this attribute") | ||||
|                 .click(async () => { | ||||
|                 if (valueAttr.attributeId) { | ||||
|                     await server.remove("notes/" + noteId + "/attributes/" + valueAttr.attributeId); | ||||
|                 } | ||||
|  | ||||
|                 $tr.remove(); | ||||
|             }); | ||||
|  | ||||
|             $multiplicityCell.append(addButton).append("   ").append(removeButton); | ||||
|         } | ||||
|  | ||||
|         return $tr; | ||||
|     } | ||||
|  | ||||
|     if (promoted.length > 0) { | ||||
|         const $tbody = $("<tbody>"); | ||||
|  | ||||
|         for (const definitionAttr of promoted) { | ||||
|             const definitionType = definitionAttr.type; | ||||
|             const valueType = definitionType.substr(0, definitionType.length - 11); | ||||
|  | ||||
|             let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType); | ||||
|  | ||||
|             if (valueAttrs.length === 0) { | ||||
|                 valueAttrs.push({ | ||||
|                     attributeId: "", | ||||
|                     type: valueType, | ||||
|                     name: definitionAttr.name, | ||||
|                     value: "" | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if (definitionAttr.value.multiplicityType === 'singlevalue') { | ||||
|                 valueAttrs = valueAttrs.slice(0, 1); | ||||
|             } | ||||
|  | ||||
|             for (const valueAttr of valueAttrs) { | ||||
|                 const $tr = await createRow(definitionAttr, valueAttr); | ||||
|  | ||||
|                 $tbody.append($tr); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // we replace the whole content in one step so there can't be any race conditions | ||||
|         // (previously we saw promoted attributes doubling) | ||||
|         $promotedAttributesContainer.empty().append($tbody); | ||||
|     } | ||||
|     else { | ||||
|         $attributeListInner.empty(); | ||||
|  | ||||
|         if (attributes.length > 0) { | ||||
|             for (const attribute of attributes) { | ||||
|                 if (attribute.type === 'label') { | ||||
|                     $attributeListInner.append(utils.formatLabel(attribute) + " "); | ||||
|                 } | ||||
|                 else if (attribute.type === 'relation') { | ||||
|                     if (attribute.value) { | ||||
|                         $attributeListInner.append('@' + attribute.name + "="); | ||||
|                         $attributeListInner.append(await linkService.createNoteLink(attribute.value)); | ||||
|                         $attributeListInner.append(" "); | ||||
|                     } | ||||
|                     else { | ||||
|                         messagingService.logError(`Relation ${attribute.attributeId} has empty target`); | ||||
|                     } | ||||
|                 } | ||||
|                 else if (attribute.type === 'label-definition' || attribute.type === 'relation-definition') { | ||||
|                     $attributeListInner.append(attribute.name + " definition "); | ||||
|                 } | ||||
|                 else { | ||||
|                     messagingService.logError("Unknown attr type: " + attribute.type); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             $attributeList.show(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return attributes; | ||||
| } | ||||
|  | ||||
| async function promotedAttributeChanged(event) { | ||||
|     const $attr = $(event.target); | ||||
|  | ||||
|     let value; | ||||
|  | ||||
|     if ($attr.prop("type") === "checkbox") { | ||||
|         value = $attr.is(':checked') ? "true" : "false"; | ||||
|     } | ||||
|     else if ($attr.prop("attribute-type") === "relation") { | ||||
|         const selectedPath = $attr.prop("data-selected-path"); | ||||
|  | ||||
|         if (selectedPath) { | ||||
|             value = treeUtils.getNoteIdFromNotePath(selectedPath); | ||||
|         } | ||||
|     } | ||||
|     else { | ||||
|         value = $attr.val(); | ||||
|     } | ||||
|  | ||||
|     const result = await server.put("notes/" + getCurrentNoteId() + "/attribute", { | ||||
|         attributeId: $attr.prop("attribute-id"), | ||||
|         type: $attr.prop("attribute-type"), | ||||
|         name: $attr.prop("attribute-name"), | ||||
|         value: value | ||||
|     }); | ||||
|  | ||||
|     $attr.prop("attribute-id", result.attributeId); | ||||
|  | ||||
|     infoService.showMessage("Attribute has been saved."); | ||||
| } | ||||
|  | ||||
| async function loadNote(noteId) { | ||||
|     const row = await server.get('notes/' + noteId); | ||||
|  | ||||
| @@ -590,9 +306,6 @@ export default { | ||||
|     getCurrentNoteId, | ||||
|     newNoteCreated, | ||||
|     focusOnTitle, | ||||
|     getAttributes, | ||||
|     showAttributes, | ||||
|     refreshAttributes, | ||||
|     saveNote, | ||||
|     saveNoteIfChanged, | ||||
|     noteChanged, | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import bundleService from "./bundle.js"; | ||||
| import server from "./server.js"; | ||||
| import noteDetailService from "./note_detail.js"; | ||||
| import attributeService from "./attributes.js"; | ||||
|  | ||||
| const $component = $('#note-detail-render'); | ||||
| const $noteDetailRenderHelp = $('#note-detail-render-help'); | ||||
| @@ -8,7 +9,7 @@ const $noteDetailRenderContent = $('#note-detail-render-content'); | ||||
| const $renderButton = $('#render-button'); | ||||
|  | ||||
| async function render() { | ||||
|     const attributes = await noteDetailService.getAttributes(); | ||||
|     const attributes = await attributeService.getAttributes(); | ||||
|     const renderNotes = attributes.filter(attr => | ||||
|         attr.type === 'relation' | ||||
|         && attr.name === 'renderNote' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user