mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 11:39:01 +01:00 
			
		
		
		
	widgetizing promoted attributes
This commit is contained in:
		
							parent
							
								
									657d01da95
								
							
						
					
					
						commit
						8b9c235465
					
				| @ -9,6 +9,7 @@ import server from "./server.js"; | |||||||
| import keyboardActionService from "./keyboard_actions.js"; | import keyboardActionService from "./keyboard_actions.js"; | ||||||
| import TabRowWidget from "./tab_row.js"; | import TabRowWidget from "./tab_row.js"; | ||||||
| import NoteTitleWidget from "../widgets/note_title.js"; | import NoteTitleWidget from "../widgets/note_title.js"; | ||||||
|  | import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; | ||||||
| 
 | 
 | ||||||
| class AppContext { | class AppContext { | ||||||
|     constructor() { |     constructor() { | ||||||
| @ -28,6 +29,9 @@ class AppContext { | |||||||
| 
 | 
 | ||||||
|         $("#global-menu-wrapper").after(contents); |         $("#global-menu-wrapper").after(contents); | ||||||
| 
 | 
 | ||||||
|  |         this.promotedAttributes = new PromotedAttributesWidget(this); | ||||||
|  |         $("#center-pane").prepend(this.promotedAttributes.render()); | ||||||
|  | 
 | ||||||
|         this.noteTitleWidget = new NoteTitleWidget(this); |         this.noteTitleWidget = new NoteTitleWidget(this); | ||||||
|         $("#center-pane").prepend(this.noteTitleWidget.render()); |         $("#center-pane").prepend(this.noteTitleWidget.render()); | ||||||
| 
 | 
 | ||||||
| @ -47,6 +51,7 @@ class AppContext { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.widgets.push(this.noteTitleWidget); |         this.widgets.push(this.noteTitleWidget); | ||||||
|  |         this.widgets.push(this.promotedAttributes); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     trigger(name, data) { |     trigger(name, data) { | ||||||
|  | |||||||
| @ -9,8 +9,6 @@ class Attributes { | |||||||
|      */ |      */ | ||||||
|     constructor(ctx) { |     constructor(ctx) { | ||||||
|         this.ctx = ctx; |         this.ctx = ctx; | ||||||
|         this.$promotedAttributesContainer = ctx.$tabContent.find(".note-detail-promoted-attributes"); |  | ||||||
|         this.$savedIndicator = ctx.$tabContent.find(".saved-indicator"); |  | ||||||
|         this.attributePromise = null; |         this.attributePromise = null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -24,8 +22,6 @@ class Attributes { | |||||||
| 
 | 
 | ||||||
|     async refreshAttributes() { |     async refreshAttributes() { | ||||||
|         this.reloadAttributes(); |         this.reloadAttributes(); | ||||||
| 
 |  | ||||||
|         await this.showAttributes(); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getAttributes() { |     async getAttributes() { | ||||||
| @ -36,237 +32,6 @@ class Attributes { | |||||||
|         return this.attributePromise; |         return this.attributePromise; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async showAttributes() { |  | ||||||
|         this.$promotedAttributesContainer.empty(); |  | ||||||
| 
 |  | ||||||
|         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: "" |  | ||||||
|                     }); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (definitionAttr.value.multiplicityType === 'singlevalue') { |  | ||||||
|                     valueAttrs = valueAttrs.slice(0, 1); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 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); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         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") |  | ||||||
|             .on('change', event => this.promotedAttributeChanged(event)); |  | ||||||
| 
 |  | ||||||
|         const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input)); |  | ||||||
| 
 |  | ||||||
|         const $actionCell = $("<td>"); |  | ||||||
|         const $multiplicityCell = $("<td>") |  | ||||||
|             .addClass("multiplicity") |  | ||||||
|             .attr("nowrap", true); |  | ||||||
| 
 |  | ||||||
|         $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: 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; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 $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 bx bx-trending-up") |  | ||||||
|                     .prop("title", "Open external link") |  | ||||||
|                     .on('click', () => window.open($input.val(), '_blank')); |  | ||||||
| 
 |  | ||||||
|                 $input.after($("<div>") |  | ||||||
|                     .addClass("input-group-append") |  | ||||||
|                     .append($openButton)); |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 ws.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); |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             $input.setSelectedPath(valueAttr.value); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             ws.logError("Unknown attribute type=" + valueAttr.type); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (definition.multiplicityType === "multivalue") { |  | ||||||
|             const addButton = $("<span>") |  | ||||||
|                 .addClass("bx bx-plus pointer") |  | ||||||
|                 .prop("title", "Add new attribute") |  | ||||||
|                 .on('click', async () => { |  | ||||||
|                     const $new = await this.createPromotedAttributeRow(definitionAttr, { |  | ||||||
|                         attributeId: "", |  | ||||||
|                         type: valueAttr.type, |  | ||||||
|                         name: definitionAttr.name, |  | ||||||
|                         value: "" |  | ||||||
|                     }); |  | ||||||
| 
 |  | ||||||
|                     $tr.after($new); |  | ||||||
| 
 |  | ||||||
|                     $new.find('input').trigger('focus'); |  | ||||||
|                 }); |  | ||||||
| 
 |  | ||||||
|             const removeButton = $("<span>") |  | ||||||
|                 .addClass("bx bx-trash pointer") |  | ||||||
|                 .prop("title", "Remove this attribute") |  | ||||||
|                 .on('click', async () => { |  | ||||||
|                     if (valueAttr.attributeId) { |  | ||||||
|                         await server.remove("notes/" + this.ctx.note.noteId + "/attributes/" + valueAttr.attributeId); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     $tr.remove(); |  | ||||||
|                 }); |  | ||||||
| 
 |  | ||||||
|             $multiplicityCell.append(addButton).append("  ").append(removeButton); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return $tr; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async 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.getSelectedPath(); |  | ||||||
| 
 |  | ||||||
|             value = selectedPath ? treeUtils.getNoteIdFromNotePath(selectedPath) : ""; |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             value = $attr.val(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const result = await server.put(`notes/${this.ctx.note.noteId}/attribute`, { |  | ||||||
|             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.
 |  | ||||||
|         if (this.$savedIndicator.queue().length === 0) { |  | ||||||
|             this.$savedIndicator.fadeOut(); |  | ||||||
|             this.$savedIndicator.fadeIn(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     eventReceived(name, data) { |     eventReceived(name, data) { | ||||||
|         if (!this.ctx.note) { |         if (!this.ctx.note) { | ||||||
|             return; |             return; | ||||||
|  | |||||||
							
								
								
									
										269
									
								
								src/public/javascripts/widgets/promoted_attributes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								src/public/javascripts/widgets/promoted_attributes.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,269 @@ | |||||||
|  | import server from "../services/server.js"; | ||||||
|  | import ws from "../services/ws.js"; | ||||||
|  | import treeUtils from "../services/tree_utils.js"; | ||||||
|  | import noteAutocompleteService from "../services/note_autocomplete.js"; | ||||||
|  | import TabAwareWidget from "./tab_aware_widget.js"; | ||||||
|  | 
 | ||||||
|  | const TPL = ` | ||||||
|  | <div class="promoted-attributes-wrapper"> | ||||||
|  |     <style> | ||||||
|  |     .promoted-attributes-wrapper { | ||||||
|  |         margin: auto; | ||||||
|  |         /* setting the display to block since "table" doesn't support scrolling */ | ||||||
|  |         display: block; | ||||||
|  |         /** flex-basis: content; - use once "content" is implemented by chrome */ | ||||||
|  |         flex-shrink: 0; | ||||||
|  |         flex-grow: 0; | ||||||
|  |         overflow: auto; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .promoted-attributes td, .promoted-attributes th { | ||||||
|  |         padding: 5px; | ||||||
|  |     } | ||||||
|  |     </style> | ||||||
|  |      | ||||||
|  |     <table class="promoted-attributes"></table> | ||||||
|  | </div> | ||||||
|  | `;
 | ||||||
|  | 
 | ||||||
|  | export default class PromotedAttributesWidget extends TabAwareWidget { | ||||||
|  |     doRender() { | ||||||
|  |         const $widget = $(TPL); | ||||||
|  | 
 | ||||||
|  |         this.$container = $widget.find(".promoted-attributes"); | ||||||
|  | 
 | ||||||
|  |         return $widget; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async activeTabChanged() { | ||||||
|  |         this.$container.empty(); | ||||||
|  | 
 | ||||||
|  |         const attributes = await this.tabContext.attributes.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: "" | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (definitionAttr.value.multiplicityType === 'singlevalue') { | ||||||
|  |                     valueAttrs = valueAttrs.slice(0, 1); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 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.$container.empty().append($tbody); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         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") | ||||||
|  |             .on('change', event => this.promotedAttributeChanged(event)); | ||||||
|  | 
 | ||||||
|  |         const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input)); | ||||||
|  | 
 | ||||||
|  |         const $actionCell = $("<td>"); | ||||||
|  |         const $multiplicityCell = $("<td>") | ||||||
|  |             .addClass("multiplicity") | ||||||
|  |             .attr("nowrap", true); | ||||||
|  | 
 | ||||||
|  |         $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: 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; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 $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 bx bx-trending-up") | ||||||
|  |                     .prop("title", "Open external link") | ||||||
|  |                     .on('click', () => window.open($input.val(), '_blank')); | ||||||
|  | 
 | ||||||
|  |                 $input.after($("<div>") | ||||||
|  |                     .addClass("input-group-append") | ||||||
|  |                     .append($openButton)); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 ws.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); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             $input.setSelectedPath(valueAttr.value); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             ws.logError("Unknown attribute type=" + valueAttr.type); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (definition.multiplicityType === "multivalue") { | ||||||
|  |             const addButton = $("<span>") | ||||||
|  |                 .addClass("bx bx-plus pointer") | ||||||
|  |                 .prop("title", "Add new attribute") | ||||||
|  |                 .on('click', async () => { | ||||||
|  |                     const $new = await this.createPromotedAttributeRow(definitionAttr, { | ||||||
|  |                         attributeId: "", | ||||||
|  |                         type: valueAttr.type, | ||||||
|  |                         name: definitionAttr.name, | ||||||
|  |                         value: "" | ||||||
|  |                     }); | ||||||
|  | 
 | ||||||
|  |                     $tr.after($new); | ||||||
|  | 
 | ||||||
|  |                     $new.find('input').trigger('focus'); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             const removeButton = $("<span>") | ||||||
|  |                 .addClass("bx bx-trash pointer") | ||||||
|  |                 .prop("title", "Remove this attribute") | ||||||
|  |                 .on('click', async () => { | ||||||
|  |                     if (valueAttr.attributeId) { | ||||||
|  |                         await server.remove("notes/" + this.tabContext.note.noteId + "/attributes/" + valueAttr.attributeId); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     $tr.remove(); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             $multiplicityCell.append(addButton).append("  ").append(removeButton); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $tr; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async 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.getSelectedPath(); | ||||||
|  | 
 | ||||||
|  |             value = selectedPath ? treeUtils.getNoteIdFromNotePath(selectedPath) : ""; | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             value = $attr.val(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const result = await server.put(`notes/${this.tabContext.note.noteId}/attribute`, { | ||||||
|  |             attributeId: $attr.prop("attribute-id"), | ||||||
|  |             type: $attr.prop("attribute-type"), | ||||||
|  |             name: $attr.prop("attribute-name"), | ||||||
|  |             value: value | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $attr.prop("attribute-id", result.attributeId); | ||||||
|  | 
 | ||||||
|  |         // FIXME
 | ||||||
|  |         // 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.
 | ||||||
|  |         // if (this.$savedIndicator.queue().length === 0) {
 | ||||||
|  |         //     this.$savedIndicator.fadeOut();
 | ||||||
|  |         //     this.$savedIndicator.fadeIn();
 | ||||||
|  |         // }
 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -478,21 +478,6 @@ button.icon-button { | |||||||
|     padding: 0; |     padding: 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .note-detail-promoted-attributes { |  | ||||||
|     margin: auto; |  | ||||||
|     /* setting the display to block since "table" doesn't support scrolling */ |  | ||||||
|     display: block; |  | ||||||
|     /** flex-basis: content; - use once "content" is implemented by chrome */ |  | ||||||
|     flex-shrink: 0; |  | ||||||
|     flex-grow: 0; |  | ||||||
|     max-height: 30%; |  | ||||||
|     overflow: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .note-detail-promoted-attributes td, .note-detail-promoted-attributes th { |  | ||||||
|     padding: 5px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .note-detail-image { | .note-detail-image { | ||||||
|     text-align: center; |     text-align: center; | ||||||
| } | } | ||||||
|  | |||||||
| @ -2,10 +2,6 @@ | |||||||
|     <div id="note-tab-container"> |     <div id="note-tab-container"> | ||||||
|         <div class="note-tab-content note-tab-content-template"> |         <div class="note-tab-content note-tab-content-template"> | ||||||
|             <div class="note-detail-content"> |             <div class="note-detail-content"> | ||||||
|                 <div class="note-detail-script-area"></div> |  | ||||||
| 
 |  | ||||||
|                 <table class="note-detail-promoted-attributes"></table> |  | ||||||
| 
 |  | ||||||
|                 <div class="note-detail-component-wrapper"> |                 <div class="note-detail-component-wrapper"> | ||||||
|                     <div class="note-detail-text note-detail-component"> |                     <div class="note-detail-text note-detail-component"> | ||||||
|                         <div class="note-detail-text-editor" tabindex="10000"></div> |                         <div class="note-detail-text-editor" tabindex="10000"></div> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 zadam
						zadam