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