mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	cleanup of labels & relations frontend code
This commit is contained in:
		@@ -1,222 +0,0 @@
 | 
				
			|||||||
import noteDetailService from '../services/note_detail.js';
 | 
					 | 
				
			||||||
import server from '../services/server.js';
 | 
					 | 
				
			||||||
import infoService from "../services/info.js";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const $dialog = $("#labels-dialog");
 | 
					 | 
				
			||||||
const $saveLabelsButton = $("#save-labels-button");
 | 
					 | 
				
			||||||
const $labelsBody = $('#labels-table tbody');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const labelsModel = new LabelsModel();
 | 
					 | 
				
			||||||
let labelNames = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function LabelsModel() {
 | 
					 | 
				
			||||||
    const self = this;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.labels = ko.observableArray();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.updateLabelPositions = function() {
 | 
					 | 
				
			||||||
        let position = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // we need to update positions by searching in the DOM, because order of the
 | 
					 | 
				
			||||||
        // labels in the viewmodel (self.labels()) stays the same
 | 
					 | 
				
			||||||
        $labelsBody.find('input[name="position"]').each(function() {
 | 
					 | 
				
			||||||
            const label = self.getTargetLabel(this);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            label().position = position++;
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.loadLabels = async function() {
 | 
					 | 
				
			||||||
        const noteId = noteDetailService.getCurrentNoteId();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const labels = await server.get('notes/' + noteId + '/labels');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.labels(labels.map(ko.observable));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        addLastEmptyRow();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        labelNames = await server.get('labels/names');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // label might not be rendered immediatelly so could not focus
 | 
					 | 
				
			||||||
        setTimeout(() => $(".label-name:last").focus(), 100);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $labelsBody.sortable({
 | 
					 | 
				
			||||||
            handle: '.handle',
 | 
					 | 
				
			||||||
            containment: $labelsBody,
 | 
					 | 
				
			||||||
            update: this.updateLabelPositions
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.deleteLabel = function(data, event) {
 | 
					 | 
				
			||||||
        const label = self.getTargetLabel(event.target);
 | 
					 | 
				
			||||||
        const labelData = label();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (labelData) {
 | 
					 | 
				
			||||||
            labelData.isDeleted = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            label(labelData);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            addLastEmptyRow();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function isValid() {
 | 
					 | 
				
			||||||
        for (let labels = self.labels(), i = 0; i < labels.length; i++) {
 | 
					 | 
				
			||||||
            if (self.isEmptyName(i)) {
 | 
					 | 
				
			||||||
                return false;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.save = async function() {
 | 
					 | 
				
			||||||
        // we need to defocus from input (in case of enter-triggered save) because value is updated
 | 
					 | 
				
			||||||
        // on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
 | 
					 | 
				
			||||||
        // stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
 | 
					 | 
				
			||||||
        $saveLabelsButton.focus();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!isValid()) {
 | 
					 | 
				
			||||||
            alert("Please fix all validation errors and try saving again.");
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.updateLabelPositions();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const noteId = noteDetailService.getCurrentNoteId();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const labelsToSave = self.labels()
 | 
					 | 
				
			||||||
            .map(label => label())
 | 
					 | 
				
			||||||
            .filter(label => label.labelId !== "" || label.name !== "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const labels = await server.put('notes/' + noteId + '/labels', labelsToSave);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.labels(labels.map(ko.observable));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        addLastEmptyRow();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        infoService.showMessage("Labels have been saved.");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        noteDetailService.loadLabelList();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function addLastEmptyRow() {
 | 
					 | 
				
			||||||
        const labels = self.labels().filter(attr => !attr().isDeleted);
 | 
					 | 
				
			||||||
        const last = labels.length === 0 ? null : labels[labels.length - 1]();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!last || last.name.trim() !== "" || last.value !== "") {
 | 
					 | 
				
			||||||
            self.labels.push(ko.observable({
 | 
					 | 
				
			||||||
                labelId: '',
 | 
					 | 
				
			||||||
                name: '',
 | 
					 | 
				
			||||||
                value: '',
 | 
					 | 
				
			||||||
                isDeleted: false,
 | 
					 | 
				
			||||||
                position: 0
 | 
					 | 
				
			||||||
            }));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.labelChanged = function (data, event) {
 | 
					 | 
				
			||||||
        addLastEmptyRow();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const label = self.getTargetLabel(event.target);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        label.valueHasMutated();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.isNotUnique = function(index) {
 | 
					 | 
				
			||||||
        const cur = self.labels()[index]();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (cur.name.trim() === "") {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (let labels = self.labels(), i = 0; i < labels.length; i++) {
 | 
					 | 
				
			||||||
            const label = labels[i]();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (index !== i && cur.name === label.name) {
 | 
					 | 
				
			||||||
                return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.isEmptyName = function(index) {
 | 
					 | 
				
			||||||
        const cur = self.labels()[index]();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return cur.name.trim() === "" && (cur.labelId !== "" || cur.value !== "");
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.getTargetLabel = function(target) {
 | 
					 | 
				
			||||||
        const context = ko.contextFor(target);
 | 
					 | 
				
			||||||
        const index = context.$index();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.labels()[index];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function showDialog() {
 | 
					 | 
				
			||||||
    glob.activeDialog = $dialog;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await labelsModel.loadLabels();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $dialog.dialog({
 | 
					 | 
				
			||||||
        modal: true,
 | 
					 | 
				
			||||||
        width: 800,
 | 
					 | 
				
			||||||
        height: 500
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ko.applyBindings(labelsModel, $dialog[0]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$dialog.on('focus', '.label-name', function (e) {
 | 
					 | 
				
			||||||
    if (!$(this).hasClass("ui-autocomplete-input")) {
 | 
					 | 
				
			||||||
        $(this).autocomplete({
 | 
					 | 
				
			||||||
            // shouldn't be required and autocomplete should just accept array of strings, but that fails
 | 
					 | 
				
			||||||
            // because we have overriden filter() function in autocomplete.js
 | 
					 | 
				
			||||||
            source: labelNames.map(label => {
 | 
					 | 
				
			||||||
                return {
 | 
					 | 
				
			||||||
                    label: label,
 | 
					 | 
				
			||||||
                    value: label
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            minLength: 0
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $(this).autocomplete("search", $(this).val());
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$dialog.on('focus', '.label-value', async function (e) {
 | 
					 | 
				
			||||||
    if (!$(this).hasClass("ui-autocomplete-input")) {
 | 
					 | 
				
			||||||
        const labelName = $(this).parent().parent().find('.label-name').val();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (labelName.trim() === "") {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const labelValues = await server.get('labels/values/' + encodeURIComponent(labelName));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (labelValues.length === 0) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $(this).autocomplete({
 | 
					 | 
				
			||||||
            // shouldn't be required and autocomplete should just accept array of strings, but that fails
 | 
					 | 
				
			||||||
            // because we have overriden filter() function in autocomplete.js
 | 
					 | 
				
			||||||
            source: labelValues.map(label => {
 | 
					 | 
				
			||||||
                return {
 | 
					 | 
				
			||||||
                    label: label,
 | 
					 | 
				
			||||||
                    value: label
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            minLength: 0
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $(this).autocomplete("search", $(this).val());
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
    showDialog
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@@ -1,250 +0,0 @@
 | 
				
			|||||||
import noteDetailService from '../services/note_detail.js';
 | 
					 | 
				
			||||||
import server from '../services/server.js';
 | 
					 | 
				
			||||||
import infoService from "../services/info.js";
 | 
					 | 
				
			||||||
import linkService from "../services/link.js";
 | 
					 | 
				
			||||||
import treeUtils from "../services/tree_utils.js";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const $dialog = $("#relations-dialog");
 | 
					 | 
				
			||||||
const $saveRelationsButton = $("#save-relations-button");
 | 
					 | 
				
			||||||
const $relationsBody = $('#relations-table tbody');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const relationsModel = new RelationsModel();
 | 
					 | 
				
			||||||
let relationNames = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function RelationsModel() {
 | 
					 | 
				
			||||||
    const self = this;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.relations = ko.observableArray();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.updateRelationPositions = function() {
 | 
					 | 
				
			||||||
        let position = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // we need to update positions by searching in the DOM, because order of the
 | 
					 | 
				
			||||||
        // relations in the viewmodel (self.relations()) stays the same
 | 
					 | 
				
			||||||
        $relationsBody.find('input[name="position"]').each(function() {
 | 
					 | 
				
			||||||
            const relation = self.getTargetRelation(this);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            relation().position = position++;
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function showRelations(relations) {
 | 
					 | 
				
			||||||
        for (const relation of relations) {
 | 
					 | 
				
			||||||
            relation.targetNoteId = await treeUtils.getNoteTitle(relation.targetNoteId) + " (" + relation.targetNoteId + ")";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.relations(relations.map(ko.observable));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.loadRelations = async function() {
 | 
					 | 
				
			||||||
        const noteId = noteDetailService.getCurrentNoteId();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const relations = await server.get('notes/' + noteId + '/relations');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await showRelations(relations);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        addLastEmptyRow();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        relationNames = await server.get('relations/names');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // relation might not be rendered immediatelly so could not focus
 | 
					 | 
				
			||||||
        setTimeout(() => $(".relation-name:last").focus(), 100);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $relationsBody.sortable({
 | 
					 | 
				
			||||||
            handle: '.handle',
 | 
					 | 
				
			||||||
            containment: $relationsBody,
 | 
					 | 
				
			||||||
            update: this.updateRelationPositions
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.deleteRelation = function(data, event) {
 | 
					 | 
				
			||||||
        const relation = self.getTargetRelation(event.target);
 | 
					 | 
				
			||||||
        const relationData = relation();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (relationData) {
 | 
					 | 
				
			||||||
            relationData.isDeleted = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            relation(relationData);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            addLastEmptyRow();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function isValid() {
 | 
					 | 
				
			||||||
        for (let relations = self.relations(), i = 0; i < relations.length; i++) {
 | 
					 | 
				
			||||||
            if (self.isEmptyName(i)) {
 | 
					 | 
				
			||||||
                return false;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.save = async function() {
 | 
					 | 
				
			||||||
        // we need to defocus from input (in case of enter-triggered save) because value is updated
 | 
					 | 
				
			||||||
        // on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
 | 
					 | 
				
			||||||
        // stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
 | 
					 | 
				
			||||||
        $saveRelationsButton.focus();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!isValid()) {
 | 
					 | 
				
			||||||
            alert("Please fix all validation errors and try saving again.");
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.updateRelationPositions();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const noteId = noteDetailService.getCurrentNoteId();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const relationsToSave = self.relations()
 | 
					 | 
				
			||||||
            .map(relation => relation())
 | 
					 | 
				
			||||||
            .filter(relation => relation.relationId !== "" || relation.name !== "");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        relationsToSave.forEach(relation => relation.targetNoteId = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel(relation.targetNoteId)));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        console.log(relationsToSave);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const relations = await server.put('notes/' + noteId + '/relations', relationsToSave);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await showRelations(relations);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        addLastEmptyRow();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        infoService.showMessage("Relations have been saved.");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        noteDetailService.loadRelationList();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function addLastEmptyRow() {
 | 
					 | 
				
			||||||
        const relations = self.relations().filter(attr => !attr().isDeleted);
 | 
					 | 
				
			||||||
        const last = relations.length === 0 ? null : relations[relations.length - 1]();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!last || last.name.trim() !== "" || last.targetNoteId !== "") {
 | 
					 | 
				
			||||||
            self.relations.push(ko.observable({
 | 
					 | 
				
			||||||
                relationId: '',
 | 
					 | 
				
			||||||
                name: '',
 | 
					 | 
				
			||||||
                targetNoteId: '',
 | 
					 | 
				
			||||||
                isInheritable: false,
 | 
					 | 
				
			||||||
                isDeleted: false,
 | 
					 | 
				
			||||||
                position: 0
 | 
					 | 
				
			||||||
            }));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.relationChanged = function (data, event) {
 | 
					 | 
				
			||||||
        addLastEmptyRow();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const relation = self.getTargetRelation(event.target);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        relation.valueHasMutated();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.isNotUnique = function(index) {
 | 
					 | 
				
			||||||
        const cur = self.relations()[index]();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (cur.name.trim() === "") {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (let relations = self.relations(), i = 0; i < relations.length; i++) {
 | 
					 | 
				
			||||||
            const relation = relations[i]();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (index !== i && cur.name === relation.name) {
 | 
					 | 
				
			||||||
                return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.isEmptyName = function(index) {
 | 
					 | 
				
			||||||
        const cur = self.relations()[index]();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return cur.name.trim() === "" && (cur.relationId !== "" || cur.targetNoteId !== "");
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.getTargetRelation = function(target) {
 | 
					 | 
				
			||||||
        const context = ko.contextFor(target);
 | 
					 | 
				
			||||||
        const index = context.$index();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.relations()[index];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function showDialog() {
 | 
					 | 
				
			||||||
    glob.activeDialog = $dialog;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await relationsModel.loadRelations();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $dialog.dialog({
 | 
					 | 
				
			||||||
        modal: true,
 | 
					 | 
				
			||||||
        width: 900,
 | 
					 | 
				
			||||||
        height: 500
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ko.applyBindings(relationsModel, document.getElementById('relations-dialog'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$dialog.on('focus', '.relation-name', function (e) {
 | 
					 | 
				
			||||||
    if (!$(this).hasClass("ui-autocomplete-input")) {
 | 
					 | 
				
			||||||
        $(this).autocomplete({
 | 
					 | 
				
			||||||
            // shouldn't be required and autocomplete should just accept array of strings, but that fails
 | 
					 | 
				
			||||||
            // because we have overriden filter() function in autocomplete.js
 | 
					 | 
				
			||||||
            source: relationNames.map(relation => {
 | 
					 | 
				
			||||||
                return {
 | 
					 | 
				
			||||||
                    label: relation,
 | 
					 | 
				
			||||||
                    value: relation
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            minLength: 0
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $(this).autocomplete("search", $(this).val());
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function initNoteAutocomplete($el) {
 | 
					 | 
				
			||||||
    if (!$el.hasClass("ui-autocomplete-input")) {
 | 
					 | 
				
			||||||
        await $el.autocomplete({
 | 
					 | 
				
			||||||
            source: async function (request, response) {
 | 
					 | 
				
			||||||
                const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (result.length > 0) {
 | 
					 | 
				
			||||||
                    response(result.map(row => {
 | 
					 | 
				
			||||||
                        return {
 | 
					 | 
				
			||||||
                            label: row.label,
 | 
					 | 
				
			||||||
                            value: row.label + ' (' + row.value + ')'
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }));
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                else {
 | 
					 | 
				
			||||||
                    response([{
 | 
					 | 
				
			||||||
                        label: "No results",
 | 
					 | 
				
			||||||
                        value: "No results"
 | 
					 | 
				
			||||||
                    }]);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            minLength: 0,
 | 
					 | 
				
			||||||
            select: function (event, ui) {
 | 
					 | 
				
			||||||
                if (ui.item.value === 'No results') {
 | 
					 | 
				
			||||||
                    return false;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$dialog.on('focus', '.relation-target-note-id', async function () {
 | 
					 | 
				
			||||||
    await initNoteAutocomplete($(this));
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$dialog.on('click', '.relations-show-recent-notes', async function () {
 | 
					 | 
				
			||||||
    const $autocomplete = $(this).parent().find('.relation-target-note-id');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await initNoteAutocomplete($autocomplete);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $autocomplete.autocomplete("search", "");
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
    showDialog
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
							
								
								
									
										1
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							@@ -1,6 +1,5 @@
 | 
				
			|||||||
import addLinkDialog from '../dialogs/add_link.js';
 | 
					import addLinkDialog from '../dialogs/add_link.js';
 | 
				
			||||||
import jumpToNoteDialog from '../dialogs/jump_to_note.js';
 | 
					import jumpToNoteDialog from '../dialogs/jump_to_note.js';
 | 
				
			||||||
import labelsDialog from '../dialogs/labels.js';
 | 
					 | 
				
			||||||
import attributesDialog from '../dialogs/attributes.js';
 | 
					import attributesDialog from '../dialogs/attributes.js';
 | 
				
			||||||
import noteRevisionsDialog from '../dialogs/note_revisions.js';
 | 
					import noteRevisionsDialog from '../dialogs/note_revisions.js';
 | 
				
			||||||
import noteSourceDialog from '../dialogs/note_source.js';
 | 
					import noteSourceDialog from '../dialogs/note_source.js';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,8 +12,6 @@ import recentChangesDialog from "../dialogs/recent_changes.js";
 | 
				
			|||||||
import sqlConsoleDialog from "../dialogs/sql_console.js";
 | 
					import sqlConsoleDialog from "../dialogs/sql_console.js";
 | 
				
			||||||
import searchNotesService from "./search_notes.js";
 | 
					import searchNotesService from "./search_notes.js";
 | 
				
			||||||
import attributesDialog from "../dialogs/attributes.js";
 | 
					import attributesDialog from "../dialogs/attributes.js";
 | 
				
			||||||
import labelsDialog from "../dialogs/labels.js";
 | 
					 | 
				
			||||||
import relationsDialog from "../dialogs/relations.js";
 | 
					 | 
				
			||||||
import protectedSessionService from "./protected_session.js";
 | 
					import protectedSessionService from "./protected_session.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function registerEntrypoints() {
 | 
					function registerEntrypoints() {
 | 
				
			||||||
@@ -42,12 +40,6 @@ function registerEntrypoints() {
 | 
				
			|||||||
    $(".show-attributes-button").click(attributesDialog.showDialog);
 | 
					    $(".show-attributes-button").click(attributesDialog.showDialog);
 | 
				
			||||||
    utils.bindShortcut('alt+a', attributesDialog.showDialog);
 | 
					    utils.bindShortcut('alt+a', attributesDialog.showDialog);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $(".show-labels-button").click(labelsDialog.showDialog);
 | 
					 | 
				
			||||||
    utils.bindShortcut('alt+l', labelsDialog.showDialog);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $(".show-relations-button").click(relationsDialog.showDialog);
 | 
					 | 
				
			||||||
    utils.bindShortcut('alt+r', relationsDialog.showDialog);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $("#options-button").click(optionsDialog.showDialog);
 | 
					    $("#options-button").click(optionsDialog.showDialog);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog);
 | 
					    utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,10 +29,6 @@ const $noteDetailComponentWrapper = $("#note-detail-component-wrapper");
 | 
				
			|||||||
const $noteIdDisplay = $("#note-id-display");
 | 
					const $noteIdDisplay = $("#note-id-display");
 | 
				
			||||||
const $attributeList = $("#attribute-list");
 | 
					const $attributeList = $("#attribute-list");
 | 
				
			||||||
const $attributeListInner = $("#attribute-list-inner");
 | 
					const $attributeListInner = $("#attribute-list-inner");
 | 
				
			||||||
const $labelList = $("#label-list");
 | 
					 | 
				
			||||||
const $labelListInner = $("#label-list-inner");
 | 
					 | 
				
			||||||
const $relationList = $("#relation-list");
 | 
					 | 
				
			||||||
const $relationListInner = $("#relation-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");
 | 
					const $promotedAttributesContainer = $("#note-detail-promoted-attributes");
 | 
				
			||||||
@@ -187,18 +183,14 @@ async function loadNoteDetail(noteId) {
 | 
				
			|||||||
    // after loading new note make sure editor is scrolled to the top
 | 
					    // after loading new note make sure editor is scrolled to the top
 | 
				
			||||||
    $noteDetailWrapper.scrollTop(0);
 | 
					    $noteDetailWrapper.scrollTop(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const labels = await loadLabelList();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview');
 | 
					 | 
				
			||||||
    await showChildrenOverview(hideChildrenOverview);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await loadRelationList();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $scriptArea.html('');
 | 
					    $scriptArea.html('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
 | 
					    await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await loadAttributes();
 | 
					    const attributes = await loadAttributes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview');
 | 
				
			||||||
 | 
					    await showChildrenOverview(hideChildrenOverview);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function showChildrenOverview(hideChildrenOverview) {
 | 
					async function showChildrenOverview(hideChildrenOverview) {
 | 
				
			||||||
@@ -411,50 +403,8 @@ async function loadAttributes() {
 | 
				
			|||||||
            $attributeList.show();
 | 
					            $attributeList.show();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function loadLabelList() {
 | 
					    return attributes;
 | 
				
			||||||
    const noteId = getCurrentNoteId();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const labels = await server.get('notes/' + noteId + '/labels');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $labelListInner.html('');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (labels.length > 0) {
 | 
					 | 
				
			||||||
        for (const label of labels) {
 | 
					 | 
				
			||||||
            $labelListInner.append(utils.formatLabel(label) + " ");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $labelList.show();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        $labelList.hide();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return labels;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function loadRelationList() {
 | 
					 | 
				
			||||||
    const noteId = getCurrentNoteId();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const relations = await server.get('notes/' + noteId + '/relations');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $relationListInner.html('');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (relations.length > 0) {
 | 
					 | 
				
			||||||
        for (const relation of relations) {
 | 
					 | 
				
			||||||
            $relationListInner.append(relation.name + " = ");
 | 
					 | 
				
			||||||
            $relationListInner.append(await linkService.createNoteLink(relation.targetNoteId));
 | 
					 | 
				
			||||||
            $relationListInner.append(" ");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $relationList.show();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        $relationList.hide();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return relations;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function loadNote(noteId) {
 | 
					async function loadNote(noteId) {
 | 
				
			||||||
@@ -535,8 +485,6 @@ export default {
 | 
				
			|||||||
    newNoteCreated,
 | 
					    newNoteCreated,
 | 
				
			||||||
    focus,
 | 
					    focus,
 | 
				
			||||||
    loadAttributes,
 | 
					    loadAttributes,
 | 
				
			||||||
    loadLabelList,
 | 
					 | 
				
			||||||
    loadRelationList,
 | 
					 | 
				
			||||||
    saveNote,
 | 
					    saveNote,
 | 
				
			||||||
    saveNoteIfChanged,
 | 
					    saveNoteIfChanged,
 | 
				
			||||||
    noteChanged
 | 
					    noteChanged
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,56 +6,7 @@ const repository = require('../../services/repository');
 | 
				
			|||||||
const Attribute = require('../../entities/attribute');
 | 
					const Attribute = require('../../entities/attribute');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getEffectiveNoteAttributes(req) {
 | 
					async function getEffectiveNoteAttributes(req) {
 | 
				
			||||||
    const noteId = req.params.noteId;
 | 
					    return await attributeService.getEffectiveAttributes(req.params.noteId);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const attributes = await repository.getEntities(`
 | 
					 | 
				
			||||||
        WITH RECURSIVE tree(noteId, level) AS (
 | 
					 | 
				
			||||||
        SELECT ?, 0
 | 
					 | 
				
			||||||
            UNION
 | 
					 | 
				
			||||||
            SELECT branches.parentNoteId, tree.level + 1 FROM branches
 | 
					 | 
				
			||||||
            JOIN tree ON branches.noteId = tree.noteId
 | 
					 | 
				
			||||||
            JOIN notes ON notes.noteId = branches.parentNoteId
 | 
					 | 
				
			||||||
            WHERE notes.isDeleted = 0 AND branches.isDeleted = 0
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        SELECT attributes.* FROM attributes JOIN tree ON attributes.noteId = tree.noteId 
 | 
					 | 
				
			||||||
        WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR attributes.noteId = ?)
 | 
					 | 
				
			||||||
        ORDER BY level, noteId, position`, [noteId, noteId]);
 | 
					 | 
				
			||||||
        // attributes are ordered so that "closest" attributes are first
 | 
					 | 
				
			||||||
        // we order by noteId so that attributes from same note stay together. Actual noteId ordering doesn't matter.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const filteredAttributes = attributes.filter((attr, index) => {
 | 
					 | 
				
			||||||
        if (attr.isDefinition()) {
 | 
					 | 
				
			||||||
            const firstDefinitionIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // keep only if this element is the first definition for this type & name
 | 
					 | 
				
			||||||
            return firstDefinitionIndex === index;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        else {
 | 
					 | 
				
			||||||
            const definitionAttr = attributes.find(el => el.type === attr.type + '-definition' && el.name === attr.name);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!definitionAttr) {
 | 
					 | 
				
			||||||
                return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const definition = definitionAttr.value;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (definition.multiplicityType === 'multivalue') {
 | 
					 | 
				
			||||||
                return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            else {
 | 
					 | 
				
			||||||
                const firstAttrIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // in case of single-valued attribute we'll keep it only if it's first (closest)
 | 
					 | 
				
			||||||
                return firstAttrIndex === index;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const attr of filteredAttributes) {
 | 
					 | 
				
			||||||
        attr.isOwned = attr.noteId === noteId;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return filteredAttributes;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function updateNoteAttribute(req) {
 | 
					async function updateNoteAttribute(req) {
 | 
				
			||||||
@@ -136,7 +87,7 @@ async function updateNoteAttributes(req) {
 | 
				
			|||||||
        await attributeEntity.save();
 | 
					        await attributeEntity.save();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return await getEffectiveNoteAttributes(req);
 | 
					    return await attributeService.getEffectiveAttributes(noteId);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getAttributeNames(req) {
 | 
					async function getAttributeNames(req) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const labelService = require('../../services/labels');
 | 
					const labelService = require('../../services/labels');
 | 
				
			||||||
const scriptService = require('../../services/script');
 | 
					const scriptService = require('../../services/script');
 | 
				
			||||||
const relationService = require('../../services/relations');
 | 
					const attributeService = require('../../services/attributes');
 | 
				
			||||||
const repository = require('../../services/repository');
 | 
					const repository = require('../../services/repository');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function exec(req) {
 | 
					async function exec(req) {
 | 
				
			||||||
@@ -40,9 +40,9 @@ async function getRelationBundles(req) {
 | 
				
			|||||||
    const noteId = req.params.noteId;
 | 
					    const noteId = req.params.noteId;
 | 
				
			||||||
    const relationName = req.params.relationName;
 | 
					    const relationName = req.params.relationName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const relations = await relationService.getEffectiveRelations(noteId);
 | 
					    const attributes = await attributeService.getEffectiveAttributes(noteId);
 | 
				
			||||||
    const filtered = relations.filter(relation => relation.name === relationName);
 | 
					    const filtered = attributes.filter(attr => attr.type === 'relation' && attr.name === relationName);
 | 
				
			||||||
    const targetNoteIds = filtered.map(relation => relation.targetNoteId);
 | 
					    const targetNoteIds = filtered.map(relation => relation.value);
 | 
				
			||||||
    const uniqueNoteIds = Array.from(new Set(targetNoteIds));
 | 
					    const uniqueNoteIds = Array.from(new Set(targetNoteIds));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const bundles = [];
 | 
					    const bundles = [];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,8 +26,6 @@ const anonymizationRoute = require('./api/anonymization');
 | 
				
			|||||||
const cleanupRoute = require('./api/cleanup');
 | 
					const cleanupRoute = require('./api/cleanup');
 | 
				
			||||||
const imageRoute = require('./api/image');
 | 
					const imageRoute = require('./api/image');
 | 
				
			||||||
const attributesRoute = require('./api/attributes');
 | 
					const attributesRoute = require('./api/attributes');
 | 
				
			||||||
const labelsRoute = require('./api/labels');
 | 
					 | 
				
			||||||
const relationsRoute = require('./api/relations');
 | 
					 | 
				
			||||||
const scriptRoute = require('./api/script');
 | 
					const scriptRoute = require('./api/script');
 | 
				
			||||||
const senderRoute = require('./api/sender');
 | 
					const senderRoute = require('./api/sender');
 | 
				
			||||||
const filesRoute = require('./api/file_upload');
 | 
					const filesRoute = require('./api/file_upload');
 | 
				
			||||||
@@ -141,15 +139,6 @@ function register(app) {
 | 
				
			|||||||
    apiRoute(GET, '/api/attributes/names', attributesRoute.getAttributeNames);
 | 
					    apiRoute(GET, '/api/attributes/names', attributesRoute.getAttributeNames);
 | 
				
			||||||
    apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute);
 | 
					    apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    apiRoute(GET, '/api/notes/:noteId/labels', labelsRoute.getNoteLabels);
 | 
					 | 
				
			||||||
    apiRoute(PUT, '/api/notes/:noteId/labels', labelsRoute.updateNoteLabels);
 | 
					 | 
				
			||||||
    apiRoute(GET, '/api/labels/names', labelsRoute.getAllLabelNames);
 | 
					 | 
				
			||||||
    apiRoute(GET, '/api/labels/values/:labelName', labelsRoute.getValuesForLabel);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    apiRoute(GET, '/api/notes/:noteId/relations', relationsRoute.getNoteRelations);
 | 
					 | 
				
			||||||
    apiRoute(PUT, '/api/notes/:noteId/relations', relationsRoute.updateNoteRelations);
 | 
					 | 
				
			||||||
    apiRoute(GET, '/api/relations/names', relationsRoute.getAllRelationNames);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    route(GET, '/api/images/:imageId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage);
 | 
					    route(GET, '/api/images/:imageId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage);
 | 
				
			||||||
    route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddleware], imageRoute.uploadImage, apiResultHandler);
 | 
					    route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddleware], imageRoute.uploadImage, apiResultHandler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ const BUILTIN_ATTRIBUTES = [
 | 
				
			|||||||
    { type: 'relation', name: 'runOnNoteTitleChange' }
 | 
					    { type: 'relation', name: 'runOnNoteTitleChange' }
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNotesWithAttribute(name, value) {
 | 
					async function getNotesWithLabel(name, value) {
 | 
				
			||||||
    let notes;
 | 
					    let notes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (value !== undefined) {
 | 
					    if (value !== undefined) {
 | 
				
			||||||
@@ -37,8 +37,8 @@ async function getNotesWithAttribute(name, value) {
 | 
				
			|||||||
    return notes;
 | 
					    return notes;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNoteWithAttribute(name, value) {
 | 
					async function getNoteWithLabel(name, value) {
 | 
				
			||||||
    const notes = await getNotesWithAttribute(name, value);
 | 
					    const notes = await getNotesWithLabel(name, value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return notes.length > 0 ? notes[0] : null;
 | 
					    return notes.length > 0 ? notes[0] : null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -70,10 +70,62 @@ async function getAttributeNames(type, nameLike) {
 | 
				
			|||||||
    return names;
 | 
					    return names;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getEffectiveAttributes(noteId) {
 | 
				
			||||||
 | 
					    const attributes = await repository.getEntities(`
 | 
				
			||||||
 | 
					        WITH RECURSIVE tree(noteId, level) AS (
 | 
				
			||||||
 | 
					        SELECT ?, 0
 | 
				
			||||||
 | 
					            UNION
 | 
				
			||||||
 | 
					            SELECT branches.parentNoteId, tree.level + 1 FROM branches
 | 
				
			||||||
 | 
					            JOIN tree ON branches.noteId = tree.noteId
 | 
				
			||||||
 | 
					            JOIN notes ON notes.noteId = branches.parentNoteId
 | 
				
			||||||
 | 
					            WHERE notes.isDeleted = 0 AND branches.isDeleted = 0
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        SELECT attributes.* FROM attributes JOIN tree ON attributes.noteId = tree.noteId 
 | 
				
			||||||
 | 
					        WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR attributes.noteId = ?)
 | 
				
			||||||
 | 
					        ORDER BY level, noteId, position`, [noteId, noteId]);
 | 
				
			||||||
 | 
					    // attributes are ordered so that "closest" attributes are first
 | 
				
			||||||
 | 
					    // we order by noteId so that attributes from same note stay together. Actual noteId ordering doesn't matter.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const filteredAttributes = attributes.filter((attr, index) => {
 | 
				
			||||||
 | 
					        if (attr.isDefinition()) {
 | 
				
			||||||
 | 
					            const firstDefinitionIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // keep only if this element is the first definition for this type & name
 | 
				
			||||||
 | 
					            return firstDefinitionIndex === index;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					            const definitionAttr = attributes.find(el => el.type === attr.type + '-definition' && el.name === attr.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!definitionAttr) {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const definition = definitionAttr.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (definition.multiplicityType === 'multivalue') {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else {
 | 
				
			||||||
 | 
					                const firstAttrIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // in case of single-valued attribute we'll keep it only if it's first (closest)
 | 
				
			||||||
 | 
					                return firstAttrIndex === index;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const attr of filteredAttributes) {
 | 
				
			||||||
 | 
					        attr.isOwned = attr.noteId === noteId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return filteredAttributes;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    getNotesWithAttribute,
 | 
					    getNotesWithLabel,
 | 
				
			||||||
    getNoteWithAttribute,
 | 
					    getNoteWithLabel,
 | 
				
			||||||
    createAttribute,
 | 
					    createAttribute,
 | 
				
			||||||
    getAttributeNames,
 | 
					    getAttributeNames,
 | 
				
			||||||
 | 
					    getEffectiveAttributes,
 | 
				
			||||||
    BUILTIN_ATTRIBUTES
 | 
					    BUILTIN_ATTRIBUTES
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -3,7 +3,7 @@ const noteService = require('./notes');
 | 
				
			|||||||
const sql = require('./sql');
 | 
					const sql = require('./sql');
 | 
				
			||||||
const utils = require('./utils');
 | 
					const utils = require('./utils');
 | 
				
			||||||
const dateUtils = require('./date_utils');
 | 
					const dateUtils = require('./date_utils');
 | 
				
			||||||
const labelService = require('./labels');
 | 
					const attributeService = require('./attributes');
 | 
				
			||||||
const dateNoteService = require('./date_notes');
 | 
					const dateNoteService = require('./date_notes');
 | 
				
			||||||
const treeService = require('./tree');
 | 
					const treeService = require('./tree');
 | 
				
			||||||
const config = require('./config');
 | 
					const config = require('./config');
 | 
				
			||||||
@@ -45,15 +45,14 @@ function ScriptApi(startNote, currentNote, workNote) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    this.getNote = repository.getNote;
 | 
					    this.getNote = repository.getNote;
 | 
				
			||||||
    this.getBranch = repository.getBranch;
 | 
					    this.getBranch = repository.getBranch;
 | 
				
			||||||
    this.getLabel = repository.getLabel;
 | 
					    this.getAttribute = repository.getAttribute;
 | 
				
			||||||
    this.getRelation = repository.getRelation;
 | 
					 | 
				
			||||||
    this.getImage = repository.getImage;
 | 
					    this.getImage = repository.getImage;
 | 
				
			||||||
    this.getEntity = repository.getEntity;
 | 
					    this.getEntity = repository.getEntity;
 | 
				
			||||||
    this.getEntities = repository.getEntities;
 | 
					    this.getEntities = repository.getEntities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.createLabel = labelService.createLabel;
 | 
					    this.createAttribute = attributeService.createAttribute;
 | 
				
			||||||
    this.getNotesWithLabel = labelService.getNotesWithLabel;
 | 
					    this.getNotesWithLabel = attributeService.getNotesWithLabel;
 | 
				
			||||||
    this.getNoteWithLabel = labelService.getNoteWithLabel;
 | 
					    this.getNoteWithLabel = attributeService.getNoteWithLabel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.createNote = noteService.createNote;
 | 
					    this.createNote = noteService.createNote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -168,10 +168,8 @@
 | 
				
			|||||||
                <span class="caret"></span>
 | 
					                <span class="caret"></span>
 | 
				
			||||||
              </button>
 | 
					              </button>
 | 
				
			||||||
              <ul class="dropdown-menu dropdown-menu-right">
 | 
					              <ul class="dropdown-menu dropdown-menu-right">
 | 
				
			||||||
                <li><a id="show-note-revisions-button">Note revisions</a></li>
 | 
					                <li><a id="show-note-revisions-button">Revisions</a></li>
 | 
				
			||||||
                <li><a class="show-attributes-button"><kbd>Alt+A</kbd> Attributes</a></li>
 | 
					                <li><a class="show-attributes-button"><kbd>Alt+A</kbd> Attributes</a></li>
 | 
				
			||||||
                <li><a class="show-labels-button"><kbd>Alt+L</kbd> Labels</a></li>
 | 
					 | 
				
			||||||
                <li><a class="show-relations-button"><kbd>Alt+R</kbd> Relations</a></li>
 | 
					 | 
				
			||||||
                <li><a id="show-source-button">HTML source</a></li>
 | 
					                <li><a id="show-source-button">HTML source</a></li>
 | 
				
			||||||
                <li><a id="upload-file-button">Upload file</a></li>
 | 
					                <li><a id="upload-file-button">Upload file</a></li>
 | 
				
			||||||
              </ul>
 | 
					              </ul>
 | 
				
			||||||
@@ -264,20 +262,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          <span id="attribute-list-inner"></span>
 | 
					          <span id="attribute-list-inner"></span>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div id="labels-and-relations" style="display: none;">
 | 
					 | 
				
			||||||
          <span id="label-list">
 | 
					 | 
				
			||||||
            <button class="btn btn-sm show-labels-button">Labels:</button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <span id="label-list-inner"></span>
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <span id="relation-list">
 | 
					 | 
				
			||||||
            <button class="btn btn-sm show-relations-button">Relations:</button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <span id="relation-list-inner"></span>
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -667,105 +651,6 @@
 | 
				
			|||||||
      </form>
 | 
					      </form>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div id="labels-dialog" title="Note labels" style="display: none; padding: 20px;">
 | 
					 | 
				
			||||||
      <form data-bind="submit: save">
 | 
					 | 
				
			||||||
      <div style="text-align: center">
 | 
					 | 
				
			||||||
        <button class="btn btn-large" style="width: 200px;" id="save-labels-button" type="submit">Save changes <kbd>enter</kbd></button>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div style="height: 97%; overflow: auto">
 | 
					 | 
				
			||||||
        <table id="labels-table" class="table">
 | 
					 | 
				
			||||||
          <thead>
 | 
					 | 
				
			||||||
            <tr>
 | 
					 | 
				
			||||||
              <th></th>
 | 
					 | 
				
			||||||
              <th>ID</th>
 | 
					 | 
				
			||||||
              <th>Name</th>
 | 
					 | 
				
			||||||
              <th>Value</th>
 | 
					 | 
				
			||||||
              <th></th>
 | 
					 | 
				
			||||||
            </tr>
 | 
					 | 
				
			||||||
          </thead>
 | 
					 | 
				
			||||||
          <tbody data-bind="foreach: labels">
 | 
					 | 
				
			||||||
            <tr data-bind="if: !isDeleted">
 | 
					 | 
				
			||||||
              <td class="handle">
 | 
					 | 
				
			||||||
                <span class="glyphicon glyphicon-resize-vertical"></span>
 | 
					 | 
				
			||||||
                <input type="hidden" name="position" data-bind="value: position"/>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
              <!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
 | 
					 | 
				
			||||||
              <td data-bind="text: labelId" style="width: 150px;"></td>
 | 
					 | 
				
			||||||
              <td>
 | 
					 | 
				
			||||||
                <!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
 | 
					 | 
				
			||||||
                <input type="text" class="label-name form-control" data-bind="value: name, valueUpdate: 'blur',  event: { blur: $parent.labelChanged }"/>
 | 
					 | 
				
			||||||
                <div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate label.</div>
 | 
					 | 
				
			||||||
                <div style="color: red" data-bind="if: $parent.isEmptyName($index())">Label name can't be empty.</div>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
              <td>
 | 
					 | 
				
			||||||
                <input type="text" class="label-value form-control" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.labelChanged }" style="width: 300px"/>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
              <td title="Delete" style="padding: 13px; cursor: pointer;">
 | 
					 | 
				
			||||||
                <span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteLabel"></span>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
            </tr>
 | 
					 | 
				
			||||||
          </tbody>
 | 
					 | 
				
			||||||
        </table>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      </form>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div id="relations-dialog" title="Note relations" style="display: none; padding: 20px;">
 | 
					 | 
				
			||||||
      <form data-bind="submit: save">
 | 
					 | 
				
			||||||
        <div style="text-align: center">
 | 
					 | 
				
			||||||
          <button class="btn btn-large" style="width: 200px;" id="save-relations-button" type="submit">Save changes <kbd>enter</kbd></button>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div style="height: 97%; overflow: auto">
 | 
					 | 
				
			||||||
          <table id="relations-table" class="table">
 | 
					 | 
				
			||||||
            <thead>
 | 
					 | 
				
			||||||
            <tr>
 | 
					 | 
				
			||||||
              <th></th>
 | 
					 | 
				
			||||||
              <th>ID</th>
 | 
					 | 
				
			||||||
              <th>Relation name</th>
 | 
					 | 
				
			||||||
              <th>Target note</th>
 | 
					 | 
				
			||||||
              <th>Inheritable</th>
 | 
					 | 
				
			||||||
              <th></th>
 | 
					 | 
				
			||||||
            </tr>
 | 
					 | 
				
			||||||
            </thead>
 | 
					 | 
				
			||||||
            <tbody data-bind="foreach: relations">
 | 
					 | 
				
			||||||
            <tr data-bind="if: !isDeleted">
 | 
					 | 
				
			||||||
              <td class="handle">
 | 
					 | 
				
			||||||
                <span class="glyphicon glyphicon-resize-vertical"></span>
 | 
					 | 
				
			||||||
                <input type="hidden" name="position" data-bind="value: position"/>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
              <!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
 | 
					 | 
				
			||||||
              <td data-bind="text: relationId" style="width: 150px;"></td>
 | 
					 | 
				
			||||||
              <td>
 | 
					 | 
				
			||||||
                <!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
 | 
					 | 
				
			||||||
                <input type="text" class="relation-name form-control" data-bind="value: name, valueUpdate: 'blur',  event: { blur: $parent.relationChanged }"/>
 | 
					 | 
				
			||||||
                <div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate relation.</div>
 | 
					 | 
				
			||||||
                <div style="color: red" data-bind="if: $parent.isEmptyName($index())">Relation name can't be empty.</div>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
              <td>
 | 
					 | 
				
			||||||
                <div class="input-group">
 | 
					 | 
				
			||||||
                  <input class="form-control relation-target-note-id"
 | 
					 | 
				
			||||||
                         placeholder="search for note by its name"
 | 
					 | 
				
			||||||
                         data-bind="value: targetNoteId, valueUpdate: 'blur', event: { blur: $parent.relationChanged }"
 | 
					 | 
				
			||||||
                         style="width: 300px;">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  <span class="input-group-addon relations-show-recent-notes" title="Show recent notes" style="background: url('/images/icons/clock-16.png') no-repeat center; cursor: pointer;"></span>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
              <td title="Inheritable relations are automatically inherited to the child notes">
 | 
					 | 
				
			||||||
                <input type="checkbox" value="1" data-bind="checked: isInheritable" />
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
              <td title="Delete" style="padding: 13px; cursor: pointer;">
 | 
					 | 
				
			||||||
                <span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteRelation"></span>
 | 
					 | 
				
			||||||
              </td>
 | 
					 | 
				
			||||||
            </tr>
 | 
					 | 
				
			||||||
            </tbody>
 | 
					 | 
				
			||||||
          </table>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </form>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div id="tooltip" style="display: none;"></div>
 | 
					    <div id="tooltip" style="display: none;"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <script type="text/javascript">
 | 
					    <script type="text/javascript">
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user