mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	attributes and children overview working again
This commit is contained in:
		
							
								
								
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "version": "0.31.4", |   "version": "0.31.5", | ||||||
|   "lockfileVersion": 1, |   "lockfileVersion": 1, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|   | |||||||
| @@ -152,8 +152,6 @@ noteTooltipService.setupGlobalTooltip(); | |||||||
|  |  | ||||||
| bundle.executeStartupBundles(); | bundle.executeStartupBundles(); | ||||||
|  |  | ||||||
| noteTypeService.init(); |  | ||||||
|  |  | ||||||
| linkService.init(); | linkService.init(); | ||||||
|  |  | ||||||
| noteAutocompleteService.init(); | noteAutocompleteService.init(); | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ import noteDetailService from '../services/note_detail.js'; | |||||||
| import server from '../services/server.js'; | import server from '../services/server.js'; | ||||||
| import infoService from "../services/info.js"; | import infoService from "../services/info.js"; | ||||||
| import treeUtils from "../services/tree_utils.js"; | import treeUtils from "../services/tree_utils.js"; | ||||||
| import attributeService from "../services/attributes.js"; |  | ||||||
| import attributeAutocompleteService from "../services/attribute_autocomplete.js"; | import attributeAutocompleteService from "../services/attribute_autocomplete.js"; | ||||||
|  |  | ||||||
| const $dialog = $("#attributes-dialog"); | const $dialog = $("#attributes-dialog"); | ||||||
| @@ -168,7 +167,9 @@ function AttributesModel() { | |||||||
|  |  | ||||||
|         infoService.showMessage("Attributes have been saved."); |         infoService.showMessage("Attributes have been saved."); | ||||||
|  |  | ||||||
|         attributeService.refreshAttributes(); |         const ctx = noteDetailService.getActiveContext(); | ||||||
|  |  | ||||||
|  |         ctx.attributes.refreshAttributes(); | ||||||
|  |  | ||||||
|         noteDetailService.reload(); |         noteDetailService.reload(); | ||||||
|     }; |     }; | ||||||
|   | |||||||
| @@ -4,308 +4,306 @@ import messagingService from "./messaging.js"; | |||||||
| import treeUtils from "./tree_utils.js"; | import treeUtils from "./tree_utils.js"; | ||||||
| import noteAutocompleteService from "./note_autocomplete.js"; | import noteAutocompleteService from "./note_autocomplete.js"; | ||||||
| import linkService from "./link.js"; | import linkService from "./link.js"; | ||||||
| import noteDetailService from "./note_detail.js"; |  | ||||||
|  |  | ||||||
| const $attributeList = $("#attribute-list"); | class Attributes { | ||||||
| const $attributeListInner = $("#attribute-list-inner"); |     /** | ||||||
| const $promotedAttributesContainer = $("#note-detail-promoted-attributes"); |      * @param {NoteContext} ctx | ||||||
| const $savedIndicator = $(".saved-indicator"); |      */ | ||||||
|  |     constructor(ctx) { | ||||||
| let attributePromise; |         this.ctx = ctx; | ||||||
|  |         this.$attributeList = ctx.$noteTabContent.find(".attribute-list"); | ||||||
| function invalidateAttributes() { |         this.$attributeListInner = ctx.$noteTabContent.find(".attribute-list-inner"); | ||||||
|     attributePromise = null; |         this.$promotedAttributesContainer = ctx.$noteTabContent.find(".note-detail-promoted-attributes"); | ||||||
| } |         this.$savedIndicator = ctx.$noteTabContent.find(".saved-indicator"); | ||||||
|  |         this.attributePromise = null; | ||||||
| function reloadAttributes() { |  | ||||||
|     attributePromise = server.get('notes/' + noteDetailService.getActiveNoteId() + '/attributes'); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function refreshAttributes() { |  | ||||||
|     reloadAttributes(); |  | ||||||
|  |  | ||||||
|     await showAttributes(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function getAttributes() { |  | ||||||
|     if (!attributePromise) { |  | ||||||
|         reloadAttributes(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return await attributePromise; |     invalidateAttributes() { | ||||||
| } |         this.attributePromise = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
| async function showAttributes() { |     reloadAttributes() { | ||||||
|     // FIXME tabs |         this.attributePromise = server.get(`notes/${this.ctx.note.noteId}/attributes`); | ||||||
|     return; |     } | ||||||
|  |  | ||||||
|     $promotedAttributesContainer.empty(); |     async refreshAttributes() { | ||||||
|     $attributeList.hide(); |         this.reloadAttributes(); | ||||||
|     $attributeListInner.empty(); |  | ||||||
|  |  | ||||||
|     const note = noteDetailService.getActiveNote(); |         await this.showAttributes(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const attributes = await attributePromise; |     async getAttributes() { | ||||||
|  |         if (!this.attributePromise) { | ||||||
|     const promoted = attributes.filter(attr => |             this.reloadAttributes(); | ||||||
|         (attr.type === 'label-definition' || attr.type === 'relation-definition') |  | ||||||
|         && !attr.name.startsWith("child:") |  | ||||||
|         && attr.value.isPromoted); |  | ||||||
|  |  | ||||||
|     const hidePromotedAttributes = attributes.some(attr => attr.type === 'label' && attr.name === 'hidePromotedAttributes'); |  | ||||||
|  |  | ||||||
|     if (promoted.length > 0 && !hidePromotedAttributes) { |  | ||||||
|         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 createPromotedAttributeRow(definitionAttr, valueAttr); |  | ||||||
|  |  | ||||||
|                 $tbody.append($tr); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // we replace the whole content in one step so there can't be any race conditions |         return await this.attributePromise; | ||||||
|         // (previously we saw promoted attributes doubling) |  | ||||||
|         $promotedAttributesContainer.empty().append($tbody); |  | ||||||
|     } |     } | ||||||
|     else if (note.type !== 'relation-map') { |  | ||||||
|         // display only "own" notes |  | ||||||
|         const ownedAttributes = attributes.filter(attr => attr.noteId === note.noteId); |  | ||||||
|  |  | ||||||
|         if (ownedAttributes.length > 0) { |     async showAttributes() { | ||||||
|             for (const attribute of ownedAttributes) { |         this.$promotedAttributesContainer.empty(); | ||||||
|                 if (attribute.type === 'label') { |         this.$attributeList.hide(); | ||||||
|                     $attributeListInner.append(utils.formatLabel(attribute) + " "); |         this.$attributeListInner.empty(); | ||||||
|  |  | ||||||
|  |         const note = this.ctx.note; | ||||||
|  |  | ||||||
|  |         const attributes = await this.getAttributes(); | ||||||
|  |  | ||||||
|  |         const promoted = attributes.filter(attr => | ||||||
|  |             (attr.type === 'label-definition' || attr.type === 'relation-definition') | ||||||
|  |             && !attr.name.startsWith("child:") | ||||||
|  |             && attr.value.isPromoted); | ||||||
|  |  | ||||||
|  |         const hidePromotedAttributes = attributes.some(attr => attr.type === 'label' && attr.name === 'hidePromotedAttributes'); | ||||||
|  |  | ||||||
|  |         if (promoted.length > 0 && !hidePromotedAttributes) { | ||||||
|  |             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: "" | ||||||
|  |                     }); | ||||||
|                 } |                 } | ||||||
|                 else if (attribute.type === 'relation') { |  | ||||||
|                     if (attribute.value) { |                 if (definitionAttr.value.multiplicityType === 'singlevalue') { | ||||||
|                         $attributeListInner.append('@' + attribute.name + "="); |                     valueAttrs = valueAttrs.slice(0, 1); | ||||||
|                         $attributeListInner.append(await linkService.createNoteLink(attribute.value)); |                 } | ||||||
|                         $attributeListInner.append(" "); |  | ||||||
|  |                 for (const valueAttr of valueAttrs) { | ||||||
|  |                     const $tr = await this.createPromotedAttributeRow(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) | ||||||
|  |             this.$promotedAttributesContainer.empty().append($tbody); | ||||||
|  |         } | ||||||
|  |         else if (note.type !== 'relation-map') { | ||||||
|  |             // display only "own" notes | ||||||
|  |             const ownedAttributes = attributes.filter(attr => attr.noteId === note.noteId); | ||||||
|  |  | ||||||
|  |             if (ownedAttributes.length > 0) { | ||||||
|  |                 for (const attribute of ownedAttributes) { | ||||||
|  |                     if (attribute.type === 'label') { | ||||||
|  |                         this.$attributeListInner.append(utils.formatLabel(attribute) + " "); | ||||||
|  |                     } | ||||||
|  |                     else if (attribute.type === 'relation') { | ||||||
|  |                         if (attribute.value) { | ||||||
|  |                             this.$attributeListInner.append('@' + attribute.name + "="); | ||||||
|  |                             this.$attributeListInner.append(await linkService.createNoteLink(attribute.value)); | ||||||
|  |                             this.$attributeListInner.append(" "); | ||||||
|  |                         } | ||||||
|  |                         else { | ||||||
|  |                             messagingService.logError(`Relation ${attribute.attributeId} has empty target`); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     else if (attribute.type === 'label-definition' || attribute.type === 'relation-definition') { | ||||||
|  |                         this.$attributeListInner.append(attribute.name + " definition "); | ||||||
|                     } |                     } | ||||||
|                     else { |                     else { | ||||||
|                         messagingService.logError(`Relation ${attribute.attributeId} has empty target`); |                         messagingService.logError("Unknown attr type: " + attribute.type); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 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(); |                 this.$attributeList.show(); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         return attributes; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return attributes; |     async createPromotedAttributeRow(definitionAttr, valueAttr) { | ||||||
| } |         const definition = definitionAttr.value; | ||||||
|  |         const $tr = $("<tr>"); | ||||||
|  |         const $labelCell = $("<th>").append(valueAttr.name); | ||||||
|  |         const $input = $("<input>") | ||||||
|  |             .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(event => this.promotedAttributeChanged(event)); | ||||||
|  |  | ||||||
| async function createPromotedAttributeRow(definitionAttr, valueAttr) { |         const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input)); | ||||||
|     const definition = definitionAttr.value; |  | ||||||
|     const $tr = $("<tr>"); |  | ||||||
|     const $labelCell = $("<th>").append(valueAttr.name); |  | ||||||
|     const $input = $("<input>") |  | ||||||
|         .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); |  | ||||||
|  |  | ||||||
|     const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input)); |         const $actionCell = $("<td>"); | ||||||
|  |         const $multiplicityCell = $("<td>") | ||||||
|  |             .addClass("multiplicity") | ||||||
|  |             .attr("nowrap", true); | ||||||
|  |  | ||||||
|     const $actionCell = $("<td>"); |         $tr | ||||||
|     const $multiplicityCell = $("<td>") |             .append($labelCell) | ||||||
|         .addClass("multiplicity") |             .append($inputCell) | ||||||
|         .attr("nowrap", true); |             .append($actionCell) | ||||||
|  |             .append($multiplicityCell); | ||||||
|  |  | ||||||
|     $tr |         if (valueAttr.type === 'label') { | ||||||
|         .append($labelCell) |             if (definition.labelType === 'text') { | ||||||
|         .append($inputCell) |                 $input.prop("type", "text"); | ||||||
|         .append($actionCell) |  | ||||||
|         .append($multiplicityCell); |  | ||||||
|  |  | ||||||
|     if (valueAttr.type === 'label') { |                 // no need to await for this, can be done asynchronously | ||||||
|         if (definition.labelType === 'text') { |                 server.get('attributes/values/' + encodeURIComponent(valueAttr.name)).then(attributeValues => { | ||||||
|             $input.prop("type", "text"); |                     if (attributeValues.length === 0) { | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|             // no need to await for this, can be done asynchronously |                     attributeValues = attributeValues.map(attribute => { return { value: attribute }; }); | ||||||
|             server.get('attributes/values/' + encodeURIComponent(valueAttr.name)).then(attributeValues => { |  | ||||||
|                 if (attributeValues.length === 0) { |                     $input.autocomplete({ | ||||||
|                     return; |                         appendTo: document.querySelector('body'), | ||||||
|  |                         hint: false, | ||||||
|  |                         autoselect: false, | ||||||
|  |                         openOnFocus: true, | ||||||
|  |                         minLength: 0, | ||||||
|  |                         tabAutocomplete: false | ||||||
|  |                     }, [{ | ||||||
|  |                         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"); | ||||||
|  |  | ||||||
|  |                 let step = 1; | ||||||
|  |  | ||||||
|  |                 for (let i = 0; i < (definition.numberPrecision || 0) && i < 10; i++) { | ||||||
|  |                     step /= 10; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 attributeValues = attributeValues.map(attribute => { return { value: attribute }; }); |                 $input.prop("step", step); | ||||||
|  |             } | ||||||
|  |             else if (definition.labelType === 'boolean') { | ||||||
|  |                 $input.prop("type", "checkbox"); | ||||||
|  |  | ||||||
|                 $input.autocomplete({ |                 if (valueAttr.value === "true") { | ||||||
|                     appendTo: document.querySelector('body'), |                     $input.prop("checked", "checked"); | ||||||
|                     hint: false, |                 } | ||||||
|                     autoselect: false, |             } | ||||||
|                     openOnFocus: true, |             else if (definition.labelType === 'date') { | ||||||
|                     minLength: 0, |                 $input.prop("type", "date"); | ||||||
|                     tabAutocomplete: false |             } | ||||||
|                 }, [{ |             else if (definition.labelType === 'url') { | ||||||
|                     displayKey: 'value', |                 $input.prop("placeholder", "http://website..."); | ||||||
|                     source: function (term, cb) { |  | ||||||
|                         term = term.toLowerCase(); |  | ||||||
|  |  | ||||||
|                         const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term)); |                 const $openButton = $("<span>") | ||||||
|  |                     .addClass("input-group-text open-external-link-button jam jam-arrow-up-right") | ||||||
|  |                     .prop("title", "Open external link") | ||||||
|  |                     .click(() => window.open($input.val(), '_blank')); | ||||||
|  |  | ||||||
|                         cb(filtered); |                 $input.after($("<div>") | ||||||
|                     } |                     .addClass("input-group-append") | ||||||
|                 }]); |                     .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', (event, suggestion, dataset) => { | ||||||
|  |                 this.promotedAttributeChanged(event); | ||||||
|             }); |             }); | ||||||
|         } |  | ||||||
|         else if (definition.labelType === 'number') { |  | ||||||
|             $input.prop("type", "number"); |  | ||||||
|  |  | ||||||
|             let step = 1; |             $input.setSelectedPath(valueAttr.value); | ||||||
|  |  | ||||||
|             for (let i = 0; i < (definition.numberPrecision || 0) && i < 10; i++) { |  | ||||||
|                 step /= 10; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             $input.prop("step", step); |  | ||||||
|         } |  | ||||||
|         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"); |  | ||||||
|         } |  | ||||||
|         else if (definition.labelType === 'url') { |  | ||||||
|             $input.prop("placeholder", "http://website..."); |  | ||||||
|  |  | ||||||
|             const $openButton = $("<span>") |  | ||||||
|                 .addClass("input-group-text open-external-link-button jam jam-arrow-up-right") |  | ||||||
|                 .prop("title", "Open external link") |  | ||||||
|                 .click(() => window.open($input.val(), '_blank')); |  | ||||||
|  |  | ||||||
|             $input.after($("<div>") |  | ||||||
|                 .addClass("input-group-append") |  | ||||||
|                 .append($openButton)); |  | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             messagingService.logError("Unknown labelType=" + definitionAttr.labelType); |             messagingService.logError("Unknown attribute type=" + valueAttr.type); | ||||||
|         } |             return; | ||||||
|     } |  | ||||||
|     else if (valueAttr.type === 'relation') { |  | ||||||
|         if (valueAttr.value) { |  | ||||||
|             $input.val(await treeUtils.getNoteTitle(valueAttr.value)); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // no need to wait for this |         if (definition.multiplicityType === "multivalue") { | ||||||
|         noteAutocompleteService.initNoteAutocomplete($input); |             const addButton = $("<span>") | ||||||
|  |                 .addClass("jam jam-plus pointer") | ||||||
|  |                 .prop("title", "Add new attribute") | ||||||
|  |                 .click(async () => { | ||||||
|  |                     const $new = await this.createPromotedAttributeRow(definitionAttr, { | ||||||
|  |                         attributeId: "", | ||||||
|  |                         type: valueAttr.type, | ||||||
|  |                         name: definitionAttr.name, | ||||||
|  |                         value: "" | ||||||
|  |                     }); | ||||||
|  |  | ||||||
|         $input.on('autocomplete:selected', function(event, suggestion, dataset) { |                     $tr.after($new); | ||||||
|             promotedAttributeChanged(event); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         $input.setSelectedPath(valueAttr.value); |                     $new.find('input').focus(); | ||||||
|     } |  | ||||||
|     else { |  | ||||||
|         messagingService.logError("Unknown attribute type=" + valueAttr.type); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (definition.multiplicityType === "multivalue") { |  | ||||||
|         const addButton = $("<span>") |  | ||||||
|             .addClass("jam jam-plus pointer") |  | ||||||
|             .prop("title", "Add new attribute") |  | ||||||
|             .click(async () => { |  | ||||||
|                 const $new = await createPromotedAttributeRow(definitionAttr, { |  | ||||||
|                     attributeId: "", |  | ||||||
|                     type: valueAttr.type, |  | ||||||
|                     name: definitionAttr.name, |  | ||||||
|                     value: "" |  | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|                 $tr.after($new); |             const removeButton = $("<span>") | ||||||
|  |                 .addClass("jam jam-trash-alt pointer") | ||||||
|  |                 .prop("title", "Remove this attribute") | ||||||
|  |                 .click(async () => { | ||||||
|  |                     if (valueAttr.attributeId) { | ||||||
|  |                         await server.remove("notes/" + noteId + "/attributes/" + valueAttr.attributeId); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|                 $new.find('input').focus(); |                     $tr.remove(); | ||||||
|             }); |                 }); | ||||||
|  |  | ||||||
|         const removeButton = $("<span>") |             $multiplicityCell.append(addButton).append("  ").append(removeButton); | ||||||
|             .addClass("jam jam-trash-alt pointer") |         } | ||||||
|             .prop("title", "Remove this attribute") |  | ||||||
|             .click(async () => { |  | ||||||
|                 if (valueAttr.attributeId) { |  | ||||||
|                     await server.remove("notes/" + noteId + "/attributes/" + valueAttr.attributeId); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $tr.remove(); |         return $tr; | ||||||
|             }); |  | ||||||
|  |  | ||||||
|         $multiplicityCell.append(addButton).append("  ").append(removeButton); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return $tr; |     async promotedAttributeChanged(event) { | ||||||
| } |         const $attr = $(event.target); | ||||||
|  |  | ||||||
| async function promotedAttributeChanged(event) { |         let value; | ||||||
|     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.getSelectedPath(); | ||||||
|  |  | ||||||
|     if ($attr.prop("type") === "checkbox") { |             value = selectedPath ? treeUtils.getNoteIdFromNotePath(selectedPath) : ""; | ||||||
|         value = $attr.is(':checked') ? "true" : "false"; |         } | ||||||
|     } |         else { | ||||||
|     else if ($attr.prop("attribute-type") === "relation") { |             value = $attr.val(); | ||||||
|         const selectedPath = $attr.getSelectedPath(); |         } | ||||||
|  |  | ||||||
|         value = selectedPath ? treeUtils.getNoteIdFromNotePath(selectedPath) : ""; |         const result = await server.put(`notes/${this.ctx.note.noteId}/attribute`, { | ||||||
|     } |             attributeId: $attr.prop("attribute-id"), | ||||||
|     else { |             type: $attr.prop("attribute-type"), | ||||||
|         value = $attr.val(); |             name: $attr.prop("attribute-name"), | ||||||
|     } |             value: value | ||||||
|  |         }); | ||||||
|  |  | ||||||
|     const result = await server.put("notes/" + noteDetailService.getActiveNoteId() + "/attribute", { |         $attr.prop("attribute-id", result.attributeId); | ||||||
|         attributeId: $attr.prop("attribute-id"), |  | ||||||
|         type: $attr.prop("attribute-type"), |  | ||||||
|         name: $attr.prop("attribute-name"), |  | ||||||
|         value: value |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     $attr.prop("attribute-id", result.attributeId); |         // animate only if it's not being animated already, this is important especially for e.g. number inputs | ||||||
|  |         // which can be changed many times in a second by clicking on higher/lower buttons. | ||||||
|     // animate only if it's not being animated already, this is important especially for e.g. number inputs |         if (this.$savedIndicator.queue().length === 0) { | ||||||
|     // which can be changed many times in a second by clicking on higher/lower buttons. |             this.$savedIndicator.fadeOut(); | ||||||
|     if ($savedIndicator.queue().length === 0) { |             this.$savedIndicator.fadeIn(); | ||||||
|         $savedIndicator.fadeOut(); |         } | ||||||
|         $savedIndicator.fadeIn(); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| export default { | export default Attributes; | ||||||
|     getAttributes, |  | ||||||
|     showAttributes, |  | ||||||
|     refreshAttributes, |  | ||||||
|     invalidateAttributes |  | ||||||
| } |  | ||||||
| @@ -2,9 +2,11 @@ import treeService from "./tree.js"; | |||||||
| import protectedSessionHolder from "./protected_session_holder.js"; | import protectedSessionHolder from "./protected_session_holder.js"; | ||||||
| import server from "./server.js"; | import server from "./server.js"; | ||||||
| import bundleService from "./bundle.js"; | import bundleService from "./bundle.js"; | ||||||
| import attributeService from "./attributes.js"; | import Attributes from "./attributes.js"; | ||||||
| import treeUtils from "./tree_utils.js"; | import treeUtils from "./tree_utils.js"; | ||||||
| import utils from "./utils.js"; | import utils from "./utils.js"; | ||||||
|  | import {NoteTypeContext} from "./note_type.js"; | ||||||
|  | import noteDetailService from "./note_detail.js"; | ||||||
| import noteDetailCode from "./note_detail_code.js"; | import noteDetailCode from "./note_detail_code.js"; | ||||||
| import noteDetailText from "./note_detail_text.js"; | import noteDetailText from "./note_detail_text.js"; | ||||||
| import noteDetailFile from "./note_detail_file.js"; | import noteDetailFile from "./note_detail_file.js"; | ||||||
| @@ -44,6 +46,8 @@ class NoteContext { | |||||||
|         this.$savedIndicator = this.$noteTabContent.find(".saved-indicator"); |         this.$savedIndicator = this.$noteTabContent.find(".saved-indicator"); | ||||||
|         this.noteChangeDisabled = false; |         this.noteChangeDisabled = false; | ||||||
|         this.isNoteChanged = false; |         this.isNoteChanged = false; | ||||||
|  |         this.attributes = new Attributes(this); | ||||||
|  |         this.noteType = new NoteTypeContext(this); | ||||||
|         this.components = {}; |         this.components = {}; | ||||||
|  |  | ||||||
|         this.$noteTitle.on('input', () => { |         this.$noteTitle.on('input', () => { | ||||||
| @@ -70,6 +74,8 @@ class NoteContext { | |||||||
|         this.$noteTabContent.attr('data-note-id', note.noteId); |         this.$noteTabContent.attr('data-note-id', note.noteId); | ||||||
|  |  | ||||||
|         chromeTabs.updateTab(this.tab, {title: note.title}); |         chromeTabs.updateTab(this.tab, {title: note.title}); | ||||||
|  |  | ||||||
|  |         this.attributes.invalidateAttributes(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     getComponent(type) { |     getComponent(type) { | ||||||
| @@ -90,7 +96,7 @@ class NoteContext { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         this.note.title = this.$noteTitle.val(); |         this.note.title = this.$noteTitle.val(); | ||||||
|         this.note.content = getActiveNoteContent(this.note); |         this.note.content = noteDetailService.getActiveNoteContent(); | ||||||
|  |  | ||||||
|         // it's important to set the flag back to false immediatelly after retrieving title and content |         // it's important to set the flag back to false immediatelly after retrieving title and content | ||||||
|         // otherwise we might overwrite another change (especially async code) |         // otherwise we might overwrite another change (especially async code) | ||||||
| @@ -127,9 +133,7 @@ class NoteContext { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async showChildrenOverview() { |     async showChildrenOverview() { | ||||||
|         return; // FIXME |         const attributes = await this.attributes.getAttributes(); | ||||||
|  |  | ||||||
|         const attributes = await attributeService.getAttributes(); |  | ||||||
|         const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview') |         const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview') | ||||||
|             || this.note.type === 'relation-map' |             || this.note.type === 'relation-map' | ||||||
|             || this.note.type === 'image' |             || this.note.type === 'image' | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ import infoService from "./info.js"; | |||||||
| import treeCache from "./tree_cache.js"; | import treeCache from "./tree_cache.js"; | ||||||
| import NoteFull from "../entities/note_full.js"; | import NoteFull from "../entities/note_full.js"; | ||||||
| import bundleService from "./bundle.js"; | import bundleService from "./bundle.js"; | ||||||
| import attributeService from "./attributes.js"; |  | ||||||
| import utils from "./utils.js"; | import utils from "./utils.js"; | ||||||
| import importDialog from "../dialogs/import.js"; | import importDialog from "../dialogs/import.js"; | ||||||
|  |  | ||||||
| @@ -167,8 +166,8 @@ async function loadNoteDetail(noteId, newTab = false) { | |||||||
|         ctx.$noteTitle.val(ctx.note.title); |         ctx.$noteTitle.val(ctx.note.title); | ||||||
|  |  | ||||||
|         if (utils.isDesktop()) { |         if (utils.isDesktop()) { | ||||||
|             noteTypeService.setNoteType(ctx.note.type); |             ctx.noteType.type(ctx.note.type); | ||||||
|             noteTypeService.setNoteMime(ctx.note.mime); |             ctx.noteType.mime(ctx.note.mime); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         for (const componentType in ctx.components) { |         for (const componentType in ctx.components) { | ||||||
| @@ -204,11 +203,11 @@ async function loadNoteDetail(noteId, newTab = false) { | |||||||
|  |  | ||||||
|     await bundleService.executeRelationBundles(ctx.note, 'runOnNoteView'); |     await bundleService.executeRelationBundles(ctx.note, 'runOnNoteView'); | ||||||
|  |  | ||||||
|     // if (utils.isDesktop()) { |     if (utils.isDesktop()) { | ||||||
|     //     await attributeService.showAttributes(); |         await ctx.attributes.showAttributes(); | ||||||
|     // |  | ||||||
|     //     await ctx.showChildrenOverview(); |         await ctx.showChildrenOverview(); | ||||||
|     // } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| async function loadNote(noteId) { | async function loadNote(noteId) { | ||||||
| @@ -293,5 +292,6 @@ export default { | |||||||
|     focusAndSelectTitle, |     focusAndSelectTitle, | ||||||
|     saveNotesIfChanged, |     saveNotesIfChanged, | ||||||
|     onNoteChange, |     onNoteChange, | ||||||
|     addDetailLoadedListener |     addDetailLoadedListener, | ||||||
|  |     getActiveContext | ||||||
| }; | }; | ||||||
| @@ -8,6 +8,7 @@ class NoteDetailImage { | |||||||
|      * @param {NoteContext} ctx |      * @param {NoteContext} ctx | ||||||
|      */ |      */ | ||||||
|     constructor(ctx) { |     constructor(ctx) { | ||||||
|  |         this.ctx = ctx; | ||||||
|         this.$component = ctx.$noteTabContent.find('.note-detail-image'); |         this.$component = ctx.$noteTabContent.find('.note-detail-image'); | ||||||
|         this.$imageWrapper = ctx.$noteTabContent.find('.note-detail-image-wrapper'); |         this.$imageWrapper = ctx.$noteTabContent.find('.note-detail-image-wrapper'); | ||||||
|         this.$imageView = ctx.$noteTabContent.find('.note-detail-image-view'); |         this.$imageView = ctx.$noteTabContent.find('.note-detail-image-view'); | ||||||
| @@ -42,18 +43,16 @@ class NoteDetailImage { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async show() { |     async show() { | ||||||
|         const activeNote = noteDetailService.getActiveNote(); |         const attributes = await server.get('notes/' + this.ctx.note.noteId + '/attributes'); | ||||||
|  |  | ||||||
|         const attributes = await server.get('notes/' + activeNote.noteId + '/attributes'); |  | ||||||
|         const attributeMap = utils.toObject(attributes, l => [l.name, l.value]); |         const attributeMap = utils.toObject(attributes, l => [l.name, l.value]); | ||||||
|  |  | ||||||
|         this.$component.show(); |         this.$component.show(); | ||||||
|  |  | ||||||
|         this.$fileName.text(attributeMap.originalFileName || "?"); |         this.$fileName.text(attributeMap.originalFileName || "?"); | ||||||
|         this.$fileSize.text((attributeMap.fileSize || "?") + " bytes"); |         this.$fileSize.text((attributeMap.fileSize || "?") + " bytes"); | ||||||
|         this.$fileType.text(activeNote.mime); |         this.$fileType.text(this.ctx.note.mime); | ||||||
|  |  | ||||||
|         this.$imageView.prop("src", `api/images/${activeNote.noteId}/${activeNote.title}`); |         this.$imageView.prop("src", `api/images/${this.ctx.note.noteId}/${this.ctx.note.title}`); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     selectImage(element) { |     selectImage(element) { | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
| import libraryLoader from "./library_loader.js"; | import libraryLoader from "./library_loader.js"; | ||||||
| import noteDetailService from './note_detail.js'; |  | ||||||
| import treeService from './tree.js'; | import treeService from './tree.js'; | ||||||
| import attributeService from "./attributes.js"; |  | ||||||
|  |  | ||||||
| class NoteDetailText { | class NoteDetailText { | ||||||
|     /** |     /** | ||||||
| @@ -69,7 +67,7 @@ class NoteDetailText { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async isReadOnly() { |     async isReadOnly() { | ||||||
|         const attributes = await attributeService.getAttributes(); |         const attributes = await this.ctx.attributes.getAttributes(); | ||||||
|  |  | ||||||
|         return attributes.some(attr => attr.type === 'label' && attr.name === 'readOnly'); |         return attributes.some(attr => attr.type === 'label' && attr.name === 'readOnly'); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -4,10 +4,6 @@ import server from './server.js'; | |||||||
| import infoService from "./info.js"; | import infoService from "./info.js"; | ||||||
| import confirmDialog from "../dialogs/confirm.js"; | import confirmDialog from "../dialogs/confirm.js"; | ||||||
|  |  | ||||||
| const $executeScriptButton = $("#execute-script-button"); |  | ||||||
| const $toggleEditButton = $('#toggle-edit-button'); |  | ||||||
| const $renderButton = $('#render-button'); |  | ||||||
|  |  | ||||||
| const DEFAULT_MIME_TYPES = [ | const DEFAULT_MIME_TYPES = [ | ||||||
|     { mime: 'text/x-csrc', title: 'C' }, |     { mime: 'text/x-csrc', title: 'C' }, | ||||||
|     { mime: 'text/x-c++src', title: 'C++' }, |     { mime: 'text/x-c++src', title: 'C++' }, | ||||||
| @@ -45,15 +41,24 @@ const DEFAULT_MIME_TYPES = [ | |||||||
|     { mime: 'text/x-yaml', title: 'YAML' } |     { mime: 'text/x-yaml', title: 'YAML' } | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| let noteTypeModel; | let mimeTypes = DEFAULT_MIME_TYPES; | ||||||
|  |  | ||||||
| function NoteTypeModel() { | /** | ||||||
|  |  * @param {NoteContext} ctx | ||||||
|  |  * @constructor | ||||||
|  |  */ | ||||||
|  | function NoteTypeContext(ctx) { | ||||||
|     const self = this; |     const self = this; | ||||||
|  |  | ||||||
|  |     this.$executeScriptButton = ctx.$noteTabContent.find(".execute-script-button"); | ||||||
|  |     this.$toggleEditButton = ctx.$noteTabContent.find('.toggle-edit-button'); | ||||||
|  |     this.$renderButton = ctx.$noteTabContent.find('.render-button'); | ||||||
|  |  | ||||||
|  |     this.ctx = ctx; | ||||||
|     this.type = ko.observable('text'); |     this.type = ko.observable('text'); | ||||||
|     this.mime = ko.observable(''); |     this.mime = ko.observable(''); | ||||||
|  |  | ||||||
|     this.codeMimeTypes = ko.observableArray(DEFAULT_MIME_TYPES); |     this.codeMimeTypes = ko.observableArray(mimeTypes); | ||||||
|  |  | ||||||
|     this.typeString = function() { |     this.typeString = function() { | ||||||
|         const type = self.type(); |         const type = self.type(); | ||||||
| @@ -97,9 +102,7 @@ function NoteTypeModel() { | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     async function save() { |     async function save() { | ||||||
|         const note = noteDetailService.getActiveNote(); |         await server.put('notes/' + self.ctx.note.noteId | ||||||
|  |  | ||||||
|         await server.put('notes/' + note.noteId |  | ||||||
|             + '/type/' + encodeURIComponent(self.type()) |             + '/type/' + encodeURIComponent(self.type()) | ||||||
|             + '/mime/' + encodeURIComponent(self.mime())); |             + '/mime/' + encodeURIComponent(self.mime())); | ||||||
|  |  | ||||||
| @@ -175,32 +178,21 @@ function NoteTypeModel() { | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.updateExecuteScriptButtonVisibility = function() { |     this.updateExecuteScriptButtonVisibility = function() { | ||||||
|         $executeScriptButton.toggle(self.mime().startsWith('application/javascript')); |         self.$executeScriptButton.toggle(self.mime().startsWith('application/javascript')); | ||||||
|  |  | ||||||
|         $toggleEditButton.toggle(self.type() === 'render'); |         self.$toggleEditButton.toggle(self.type() === 'render'); | ||||||
|         $renderButton.toggle(self.type() === 'render'); |         self.$renderButton.toggle(self.type() === 'render'); | ||||||
|     } |     }; | ||||||
| } |  | ||||||
|  |  | ||||||
| function init() { |     ko.applyBindings(this, ctx.$noteTabContent.find('.note-type-wrapper')[0]) | ||||||
|     noteTypeModel = new NoteTypeModel(); |  | ||||||
|  |  | ||||||
|     ko.applyBindings(noteTypeModel, document.getElementById('note-type-wrapper')); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     getNoteType: () => noteTypeModel.type(), |  | ||||||
|     setNoteType: type => noteTypeModel.type(type), |  | ||||||
|  |  | ||||||
|     getNoteMime: () => noteTypeModel.mime(), |  | ||||||
|     setNoteMime: mime => { |  | ||||||
|         noteTypeModel.mime(mime); |  | ||||||
|  |  | ||||||
|         noteTypeModel.updateExecuteScriptButtonVisibility(); |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     getDefaultCodeMimeTypes: () => DEFAULT_MIME_TYPES.slice(), |     getDefaultCodeMimeTypes: () => DEFAULT_MIME_TYPES.slice(), | ||||||
|     getCodeMimeTypes: () => noteTypeModel.codeMimeTypes(), |     getCodeMimeTypes: () => mimeTypes, | ||||||
|     setCodeMimeTypes: types => noteTypeModel.codeMimeTypes(types), |     setCodeMimeTypes: types => { mimeTypes = types; } | ||||||
|     init | }; | ||||||
|  |  | ||||||
|  | export { | ||||||
|  |     NoteTypeContext | ||||||
| }; | }; | ||||||
| @@ -412,7 +412,7 @@ div.ui-tooltip { | |||||||
|     font-size: larger; |     font-size: larger; | ||||||
| } | } | ||||||
|  |  | ||||||
| #children-overview { | .children-overview { | ||||||
|     flex-grow: 1000; |     flex-grow: 1000; | ||||||
|     flex-shrink: 1000; |     flex-shrink: 1000; | ||||||
|     flex-basis: 0; |     flex-basis: 0; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user