mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 05:28:59 +01:00 
			
		
		
		
	refactored attributes out of note detail, fixes #213
This commit is contained in:
		
							parent
							
								
									3ba761fe28
								
							
						
					
					
						commit
						6416e3e9fb
					
				@ -2,7 +2,7 @@ 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 linkService from "../services/link.js";
 | 
					import attributeService from "../services/attributes.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const $dialog = $("#attributes-dialog");
 | 
					const $dialog = $("#attributes-dialog");
 | 
				
			||||||
const $saveAttributesButton = $("#save-attributes-button");
 | 
					const $saveAttributesButton = $("#save-attributes-button");
 | 
				
			||||||
@ -165,7 +165,7 @@ function AttributesModel() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        infoService.showMessage("Attributes have been saved.");
 | 
					        infoService.showMessage("Attributes have been saved.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        noteDetailService.refreshAttributes();
 | 
					        attributeService.refreshAttributes();
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function addLastEmptyRow() {
 | 
					    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 noteTypeService from './note_type.js';
 | 
				
			||||||
import protectedSessionService from './protected_session.js';
 | 
					import protectedSessionService from './protected_session.js';
 | 
				
			||||||
import protectedSessionHolder from './protected_session_holder.js';
 | 
					import protectedSessionHolder from './protected_session_holder.js';
 | 
				
			||||||
import utils from './utils.js';
 | 
					 | 
				
			||||||
import server from './server.js';
 | 
					import server from './server.js';
 | 
				
			||||||
import messagingService from "./messaging.js";
 | 
					import messagingService from "./messaging.js";
 | 
				
			||||||
import infoService from "./info.js";
 | 
					import infoService from "./info.js";
 | 
				
			||||||
import linkService from "./link.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 noteDetailCode from './note_detail_code.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 noteDetailRender from './note_detail_render.js';
 | 
				
			||||||
import noteDetailRelationMap from './note_detail_relation_map.js';
 | 
					import noteDetailRelationMap from './note_detail_relation_map.js';
 | 
				
			||||||
import bundleService from "./bundle.js";
 | 
					import bundleService from "./bundle.js";
 | 
				
			||||||
import noteAutocompleteService from "./note_autocomplete.js";
 | 
					import attributeService from "./attributes.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const $noteTitle = $("#note-title");
 | 
					const $noteTitle = $("#note-title");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -28,11 +26,8 @@ const $protectButton = $("#protect-button");
 | 
				
			|||||||
const $unprotectButton = $("#unprotect-button");
 | 
					const $unprotectButton = $("#unprotect-button");
 | 
				
			||||||
const $noteDetailWrapper = $("#note-detail-wrapper");
 | 
					const $noteDetailWrapper = $("#note-detail-wrapper");
 | 
				
			||||||
const $noteIdDisplay = $("#note-id-display");
 | 
					const $noteIdDisplay = $("#note-id-display");
 | 
				
			||||||
const $attributeList = $("#attribute-list");
 | 
					 | 
				
			||||||
const $attributeListInner = $("#attribute-list-inner");
 | 
					 | 
				
			||||||
const $childrenOverview = $("#children-overview");
 | 
					const $childrenOverview = $("#children-overview");
 | 
				
			||||||
const $scriptArea = $("#note-detail-script-area");
 | 
					const $scriptArea = $("#note-detail-script-area");
 | 
				
			||||||
const $promotedAttributesContainer = $("#note-detail-promoted-attributes");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
let currentNote = null;
 | 
					let currentNote = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -40,8 +35,6 @@ let noteChangeDisabled = false;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
let isNoteChanged = false;
 | 
					let isNoteChanged = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let attributePromise;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const components = {
 | 
					const components = {
 | 
				
			||||||
    'code': noteDetailCode,
 | 
					    'code': noteDetailCode,
 | 
				
			||||||
    'text': noteDetailText,
 | 
					    'text': noteDetailText,
 | 
				
			||||||
@ -181,7 +174,7 @@ async function loadNoteDetail(noteId) {
 | 
				
			|||||||
    currentNote = loadedNote;
 | 
					    currentNote = loadedNote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // needs to happend after loading the note itself because it references current noteId
 | 
					    // needs to happend after loading the note itself because it references current noteId
 | 
				
			||||||
    refreshAttributes();
 | 
					    attributeService.refreshAttributes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (isNewNoteCreated) {
 | 
					    if (isNewNoteCreated) {
 | 
				
			||||||
        isNewNoteCreated = false;
 | 
					        isNewNoteCreated = false;
 | 
				
			||||||
@ -232,14 +225,14 @@ async function loadNoteDetail(noteId) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
 | 
					    await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await showAttributes();
 | 
					    await attributeService.showAttributes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await showChildrenOverview();
 | 
					    await showChildrenOverview();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function showChildrenOverview() {
 | 
					async function showChildrenOverview() {
 | 
				
			||||||
    const note = getCurrentNote();
 | 
					    const note = getCurrentNote();
 | 
				
			||||||
    const attributes = await attributePromise;
 | 
					    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')
 | 
				
			||||||
        || note.type === 'relation-map'
 | 
					        || note.type === 'relation-map'
 | 
				
			||||||
        || note.type === 'image'
 | 
					        || note.type === 'image'
 | 
				
			||||||
@ -267,283 +260,6 @@ async function showChildrenOverview() {
 | 
				
			|||||||
    $childrenOverview.show();
 | 
					    $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) {
 | 
					async function loadNote(noteId) {
 | 
				
			||||||
    const row = await server.get('notes/' + noteId);
 | 
					    const row = await server.get('notes/' + noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -590,9 +306,6 @@ export default {
 | 
				
			|||||||
    getCurrentNoteId,
 | 
					    getCurrentNoteId,
 | 
				
			||||||
    newNoteCreated,
 | 
					    newNoteCreated,
 | 
				
			||||||
    focusOnTitle,
 | 
					    focusOnTitle,
 | 
				
			||||||
    getAttributes,
 | 
					 | 
				
			||||||
    showAttributes,
 | 
					 | 
				
			||||||
    refreshAttributes,
 | 
					 | 
				
			||||||
    saveNote,
 | 
					    saveNote,
 | 
				
			||||||
    saveNoteIfChanged,
 | 
					    saveNoteIfChanged,
 | 
				
			||||||
    noteChanged,
 | 
					    noteChanged,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import bundleService from "./bundle.js";
 | 
					import bundleService from "./bundle.js";
 | 
				
			||||||
import server from "./server.js";
 | 
					import server from "./server.js";
 | 
				
			||||||
import noteDetailService from "./note_detail.js";
 | 
					import noteDetailService from "./note_detail.js";
 | 
				
			||||||
 | 
					import attributeService from "./attributes.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const $component = $('#note-detail-render');
 | 
					const $component = $('#note-detail-render');
 | 
				
			||||||
const $noteDetailRenderHelp = $('#note-detail-render-help');
 | 
					const $noteDetailRenderHelp = $('#note-detail-render-help');
 | 
				
			||||||
@ -8,7 +9,7 @@ const $noteDetailRenderContent = $('#note-detail-render-content');
 | 
				
			|||||||
const $renderButton = $('#render-button');
 | 
					const $renderButton = $('#render-button');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function render() {
 | 
					async function render() {
 | 
				
			||||||
    const attributes = await noteDetailService.getAttributes();
 | 
					    const attributes = await attributeService.getAttributes();
 | 
				
			||||||
    const renderNotes = attributes.filter(attr =>
 | 
					    const renderNotes = attributes.filter(attr =>
 | 
				
			||||||
        attr.type === 'relation'
 | 
					        attr.type === 'relation'
 | 
				
			||||||
        && attr.name === 'renderNote'
 | 
					        && attr.name === 'renderNote'
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user