mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-11-04 15:11:31 +08:00 
			
		
		
		
	implemented mirror relations
This commit is contained in:
		
							parent
							
								
									e7cea59ba7
								
							
						
					
					
						commit
						21d3b0c9d8
					
				@ -40,10 +40,20 @@ class Attribute extends Entity {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @returns {Promise<Note|null>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    async getNote() {
 | 
					    async getNote() {
 | 
				
			||||||
        return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
					        if (!this.__note) {
 | 
				
			||||||
 | 
					            this.__note = await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.__note;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @returns {Promise<Note|null>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    async getTargetNote() {
 | 
					    async getTargetNote() {
 | 
				
			||||||
        if (this.type !== 'relation') {
 | 
					        if (this.type !== 'relation') {
 | 
				
			||||||
            throw new Error(`Attribute ${this.attributeId} is not relation`);
 | 
					            throw new Error(`Attribute ${this.attributeId} is not relation`);
 | 
				
			||||||
@ -53,9 +63,16 @@ class Attribute extends Entity {
 | 
				
			|||||||
            return null;
 | 
					            return null;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.value]);
 | 
					        if (!this.__targetNote) {
 | 
				
			||||||
 | 
					            this.__targetNote = await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.value]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.__targetNote;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @return {boolean}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    isDefinition() {
 | 
					    isDefinition() {
 | 
				
			||||||
        return this.type === 'label-definition' || this.type === 'relation-definition';
 | 
					        return this.type === 'label-definition' || this.type === 'relation-definition';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -71,6 +71,7 @@ function AttributesModel() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            attr.relationDefinition = (attr.type === 'relation-definition' && attr.value) ? attr.value : {
 | 
					            attr.relationDefinition = (attr.type === 'relation-definition' && attr.value) ? attr.value : {
 | 
				
			||||||
                multiplicityType: "singlevalue",
 | 
					                multiplicityType: "singlevalue",
 | 
				
			||||||
 | 
					                mirrorRelation: "",
 | 
				
			||||||
                isPromoted: true
 | 
					                isPromoted: true
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -189,6 +190,7 @@ function AttributesModel() {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
                relationDefinition: {
 | 
					                relationDefinition: {
 | 
				
			||||||
                    multiplicityType: "singlevalue",
 | 
					                    multiplicityType: "singlevalue",
 | 
				
			||||||
 | 
					                    mirrorRelation: "",
 | 
				
			||||||
                    isPromoted: true
 | 
					                    isPromoted: true
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }));
 | 
					            }));
 | 
				
			||||||
 | 
				
			|||||||
@ -60,7 +60,7 @@ async function showAttributes() {
 | 
				
			|||||||
        const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input));
 | 
					        const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const $actionCell = $("<td>");
 | 
					        const $actionCell = $("<td>");
 | 
				
			||||||
        const $multiplicityCell = $("<td>");
 | 
					        const $multiplicityCell = $("<td>").addClass("multiplicity");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $tr
 | 
					        $tr
 | 
				
			||||||
            .append($labelCell)
 | 
					            .append($labelCell)
 | 
				
			||||||
@ -148,9 +148,14 @@ async function showAttributes() {
 | 
				
			|||||||
            // ideally we'd use link instead of button which would allow tooltip preview, but
 | 
					            // 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
 | 
					            // we can't guarantee updating the link in the a element
 | 
				
			||||||
            const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => {
 | 
					            const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => {
 | 
				
			||||||
                const notePath = $input.prop("data-selected-path");
 | 
					                const notePath = $input.getSelectedPath();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (notePath) {
 | 
				
			||||||
                    treeService.activateNote(notePath);
 | 
					                    treeService.activateNote(notePath);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                else {
 | 
				
			||||||
 | 
					                    console.log("Empty note path, nothing to open.");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $actionCell.append($openButton);
 | 
					            $actionCell.append($openButton);
 | 
				
			||||||
@ -162,7 +167,7 @@ async function showAttributes() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if (definition.multiplicityType === "multivalue") {
 | 
					        if (definition.multiplicityType === "multivalue") {
 | 
				
			||||||
            const addButton = $("<span>")
 | 
					            const addButton = $("<span>")
 | 
				
			||||||
                .addClass("glyphicon glyphicon-plus pointer")
 | 
					                .addClass("jam jam-plus pointer")
 | 
				
			||||||
                .prop("title", "Add new attribute")
 | 
					                .prop("title", "Add new attribute")
 | 
				
			||||||
                .click(async () => {
 | 
					                .click(async () => {
 | 
				
			||||||
                    const $new = await createRow(definitionAttr, {
 | 
					                    const $new = await createRow(definitionAttr, {
 | 
				
			||||||
@ -178,7 +183,7 @@ async function showAttributes() {
 | 
				
			|||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const removeButton = $("<span>")
 | 
					            const removeButton = $("<span>")
 | 
				
			||||||
                .addClass("glyphicon glyphicon-trash pointer")
 | 
					                .addClass("jam jam-trash pointer")
 | 
				
			||||||
                .prop("title", "Remove this attribute")
 | 
					                .prop("title", "Remove this attribute")
 | 
				
			||||||
                .click(async () => {
 | 
					                .click(async () => {
 | 
				
			||||||
                    if (valueAttr.attributeId) {
 | 
					                    if (valueAttr.attributeId) {
 | 
				
			||||||
@ -269,11 +274,9 @@ async function promotedAttributeChanged(event) {
 | 
				
			|||||||
        value = $attr.is(':checked') ? "true" : "false";
 | 
					        value = $attr.is(':checked') ? "true" : "false";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if ($attr.prop("attribute-type") === "relation") {
 | 
					    else if ($attr.prop("attribute-type") === "relation") {
 | 
				
			||||||
        const selectedPath = $attr.prop("data-selected-path");
 | 
					        const selectedPath = $attr.getSelectedPath();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (selectedPath) {
 | 
					        value = selectedPath ? treeUtils.getNoteIdFromNotePath(selectedPath) : "";
 | 
				
			||||||
            value = treeUtils.getNoteIdFromNotePath(selectedPath);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else {
 | 
					    else {
 | 
				
			||||||
        value = $attr.val();
 | 
					        value = $attr.val();
 | 
				
			||||||
 | 
				
			|||||||
@ -54,24 +54,34 @@ function initNoteAutocomplete($el) {
 | 
				
			|||||||
            $el.prop("data-selected-path", suggestion.path);
 | 
					            $el.prop("data-selected-path", suggestion.path);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $el.getSelectedPath = () => $el.prop("data-selected-path");
 | 
					        $el.on('autocomplete:closed', () => {
 | 
				
			||||||
 | 
					            $el.prop("data-selected-path", "");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return $el;
 | 
					    return $el;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$.fn.getSelectedPath = function() {
 | 
				
			||||||
 | 
					    if (!$(this).val().trim()) {
 | 
				
			||||||
 | 
					        return "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					        return $(this).prop("data-selected-path");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ko.bindingHandlers.noteAutocomplete = {
 | 
					ko.bindingHandlers.noteAutocomplete = {
 | 
				
			||||||
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
 | 
					    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
 | 
				
			||||||
        initNoteAutocomplete($(element));
 | 
					        initNoteAutocomplete($(element));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $(element).on('autocomplete:selected', function(event, suggestion, dataset) {
 | 
					        $(element).on('autocomplete:selected', function(event, suggestion, dataset) {
 | 
				
			||||||
            bindingContext.$data.selectedPath = suggestion.path;
 | 
					            bindingContext.$data.selectedPath = $(element).val().trim() ? suggestion.path : '';
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    initNoteAutocomplete,
 | 
					    initNoteAutocomplete,
 | 
				
			||||||
    autocompleteSource,
 | 
					 | 
				
			||||||
    showRecentNotes
 | 
					    showRecentNotes
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -152,6 +152,11 @@ async function getRunPath(notePath) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if (childNoteId !== null) {
 | 
					        if (childNoteId !== null) {
 | 
				
			||||||
            const child = await treeCache.getNote(childNoteId);
 | 
					            const child = await treeCache.getNote(childNoteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!child) {
 | 
				
			||||||
 | 
					                console.log("Can't find " + childNoteId);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const parents = await child.getParentNotes();
 | 
					            const parents = await child.getParentNotes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!parents) {
 | 
					            if (!parents) {
 | 
				
			||||||
@ -609,7 +614,7 @@ $(window).bind('hashchange', function() {
 | 
				
			|||||||
    const notePath = getNotePathFromAddress();
 | 
					    const notePath = getNotePathFromAddress();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (getCurrentNotePath() !== notePath) {
 | 
					    if (getCurrentNotePath() !== notePath) {
 | 
				
			||||||
        console.log("Switching to " + notePath + " because of hash change");
 | 
					        console.debug("Switching to " + notePath + " because of hash change");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        activateNote(notePath);
 | 
					        activateNote(notePath);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -57,7 +57,7 @@ class TreeCache {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return noteIds.map(noteId => {
 | 
					        return noteIds.map(noteId => {
 | 
				
			||||||
            if (!this.notes[noteId] && !silentNotFoundError) {
 | 
					            if (!this.notes[noteId] && !silentNotFoundError) {
 | 
				
			||||||
                messagingService.logError(`Can't find note ${noteId}`);
 | 
					                messagingService.logError(`Can't find note "${noteId}"`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return null;
 | 
					                return null;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
				
			|||||||
@ -521,6 +521,11 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
 | 
				
			|||||||
    margin: 0;
 | 
					    margin: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.algolia-autocomplete .aa-dropdown-menu .aa-suggestion p {
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor {
 | 
					.algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor {
 | 
				
			||||||
    background-color: #B2D7FF;
 | 
					    background-color: #B2D7FF;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -545,3 +550,7 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
 | 
				
			|||||||
.fancytree-custom-icon {
 | 
					.fancytree-custom-icon {
 | 
				
			||||||
    font-size: 1.3em;
 | 
					    font-size: 1.3em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.multiplicity {
 | 
				
			||||||
 | 
					    font-size: larger;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -20,6 +20,10 @@ async function updateNoteAttribute(req) {
 | 
				
			|||||||
        attribute = await repository.getAttribute(body.attributeId);
 | 
					        attribute = await repository.getAttribute(body.attributeId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else {
 | 
					    else {
 | 
				
			||||||
 | 
					        if (body.type === 'relation' && !body.value.trim()) {
 | 
				
			||||||
 | 
					            return {};
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        attribute = new Attribute();
 | 
					        attribute = new Attribute();
 | 
				
			||||||
        attribute.noteId = noteId;
 | 
					        attribute.noteId = noteId;
 | 
				
			||||||
        attribute.name = body.name;
 | 
					        attribute.name = body.name;
 | 
				
			||||||
@ -30,7 +34,13 @@ async function updateNoteAttribute(req) {
 | 
				
			|||||||
        return [400, `Attribute ${body.attributeId} is not owned by ${noteId}`];
 | 
					        return [400, `Attribute ${body.attributeId} is not owned by ${noteId}`];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (body.value.trim()) {
 | 
				
			||||||
        attribute.value = body.value;
 | 
					        attribute.value = body.value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					        // relations should never have empty target
 | 
				
			||||||
 | 
					        attribute.isDeleted = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await attribute.save();
 | 
					    await attribute.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -81,11 +91,18 @@ async function updateNoteAttributes(req) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        attributeEntity.type = attribute.type;
 | 
					        attributeEntity.type = attribute.type;
 | 
				
			||||||
        attributeEntity.name = attribute.name;
 | 
					        attributeEntity.name = attribute.name;
 | 
				
			||||||
        attributeEntity.value = attribute.value;
 | 
					 | 
				
			||||||
        attributeEntity.position = attribute.position;
 | 
					        attributeEntity.position = attribute.position;
 | 
				
			||||||
        attributeEntity.isInheritable = attribute.isInheritable;
 | 
					        attributeEntity.isInheritable = attribute.isInheritable;
 | 
				
			||||||
        attributeEntity.isDeleted = attribute.isDeleted;
 | 
					        attributeEntity.isDeleted = attribute.isDeleted;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (attributeEntity.type === 'relation' && !attributeEntity.value.trim()) {
 | 
				
			||||||
 | 
					            // relation should never have empty target
 | 
				
			||||||
 | 
					            attributeEntity.isDeleted = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					            attributeEntity.value = attribute.value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await attributeEntity.save();
 | 
					        await attributeEntity.save();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ const sqlInit = require('./sql_init');
 | 
				
			|||||||
const log = require('./log');
 | 
					const log = require('./log');
 | 
				
			||||||
const messagingService = require('./messaging');
 | 
					const messagingService = require('./messaging');
 | 
				
			||||||
const syncMutexService = require('./sync_mutex');
 | 
					const syncMutexService = require('./sync_mutex');
 | 
				
			||||||
 | 
					const repository = require('./repository.js');
 | 
				
			||||||
const cls = require('./cls');
 | 
					const cls = require('./cls');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function runCheck(query, errorText, errorList) {
 | 
					async function runCheck(query, errorText, errorList) {
 | 
				
			||||||
@ -89,6 +90,17 @@ async function runSyncRowChecks(table, key, errorList) {
 | 
				
			|||||||
        `Missing ${table} records for existing sync rows`, errorList);
 | 
					        `Missing ${table} records for existing sync rows`, errorList);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function fixEmptyRelationTargets(errorList) {
 | 
				
			||||||
 | 
					    const emptyRelations = await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'relation' AND value = ''");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const relation of emptyRelations) {
 | 
				
			||||||
 | 
					        relation.isDeleted = true;
 | 
				
			||||||
 | 
					        await relation.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        errorList.push(`Relation ${relation.attributeId} of name "${relation.name} has empty target. Autofixed.`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function runAllChecks() {
 | 
					async function runAllChecks() {
 | 
				
			||||||
    const errorList = [];
 | 
					    const errorList = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -221,6 +233,8 @@ async function runAllChecks() {
 | 
				
			|||||||
        await checkTreeCycles(errorList);
 | 
					        await checkTreeCycles(errorList);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await fixEmptyRelationTargets(errorList);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return errorList;
 | 
					    return errorList;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED";
 | 
				
			|||||||
const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
 | 
					const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
 | 
				
			||||||
const ENTITY_CREATED = "ENTITY_CREATED";
 | 
					const ENTITY_CREATED = "ENTITY_CREATED";
 | 
				
			||||||
const ENTITY_CHANGED = "ENTITY_CHANGED";
 | 
					const ENTITY_CHANGED = "ENTITY_CHANGED";
 | 
				
			||||||
 | 
					const ENTITY_DELETED = "ENTITY_DELETED";
 | 
				
			||||||
const CHILD_NOTE_CREATED = "CHILD_NOTE_CREATED";
 | 
					const CHILD_NOTE_CREATED = "CHILD_NOTE_CREATED";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const eventListeners = {};
 | 
					const eventListeners = {};
 | 
				
			||||||
@ -37,5 +38,6 @@ module.exports = {
 | 
				
			|||||||
    ENTER_PROTECTED_SESSION,
 | 
					    ENTER_PROTECTED_SESSION,
 | 
				
			||||||
    ENTITY_CREATED,
 | 
					    ENTITY_CREATED,
 | 
				
			||||||
    ENTITY_CHANGED,
 | 
					    ENTITY_CHANGED,
 | 
				
			||||||
 | 
					    ENTITY_DELETED,
 | 
				
			||||||
    CHILD_NOTE_CREATED
 | 
					    CHILD_NOTE_CREATED
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -3,9 +3,10 @@ const scriptService = require('./script');
 | 
				
			|||||||
const treeService = require('./tree');
 | 
					const treeService = require('./tree');
 | 
				
			||||||
const messagingService = require('./messaging');
 | 
					const messagingService = require('./messaging');
 | 
				
			||||||
const log = require('./log');
 | 
					const log = require('./log');
 | 
				
			||||||
 | 
					const Attribute = require('../entities/attribute');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function runAttachedRelations(note, relationName, originEntity) {
 | 
					async function runAttachedRelations(note, relationName, originEntity) {
 | 
				
			||||||
    const runRelations = (await note.getRelations()).filter(relation => relation.name === relationName);
 | 
					    const runRelations = await note.getRelations(relationName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const relation of runRelations) {
 | 
					    for (const relation of runRelations) {
 | 
				
			||||||
        const scriptNote = await relation.getTargetNote();
 | 
					        const scriptNote = await relation.getTargetNote();
 | 
				
			||||||
@ -57,3 +58,53 @@ eventService.subscribe(eventService.ENTITY_CREATED, async ({ entityName, entity
 | 
				
			|||||||
eventService.subscribe(eventService.CHILD_NOTE_CREATED, async ({ parentNote, childNote }) => {
 | 
					eventService.subscribe(eventService.CHILD_NOTE_CREATED, async ({ parentNote, childNote }) => {
 | 
				
			||||||
    await runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote);
 | 
					    await runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function processMirrorRelations(entityName, entity, handler) {
 | 
				
			||||||
 | 
					    if (entityName === 'attributes' && entity.type === 'relation') {
 | 
				
			||||||
 | 
					        const note = await entity.getNote();
 | 
				
			||||||
 | 
					        const attributes = (await note.getAttributes(entity.name)).filter(relation => relation.type === 'relation-definition');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const attribute of attributes) {
 | 
				
			||||||
 | 
					            const definition = attribute.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (definition.mirrorRelation && definition.mirrorRelation.trim()) {
 | 
				
			||||||
 | 
					                const targetNote = await entity.getTargetNote();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                await handler(definition, note, targetNote);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => {
 | 
				
			||||||
 | 
					    await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => {
 | 
				
			||||||
 | 
					        // we need to make sure that also target's mirror attribute exists and if note, then create it
 | 
				
			||||||
 | 
					        if (!await targetNote.hasRelation(definition.mirrorRelation)) {
 | 
				
			||||||
 | 
					            await new Attribute({
 | 
				
			||||||
 | 
					                noteId: targetNote.noteId,
 | 
				
			||||||
 | 
					                type: 'relation',
 | 
				
			||||||
 | 
					                name: definition.mirrorRelation,
 | 
				
			||||||
 | 
					                value: note.noteId,
 | 
				
			||||||
 | 
					                isInheritable: entity.isInheritable
 | 
				
			||||||
 | 
					            }).save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            targetNote.invalidateAttributeCache();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					eventService.subscribe(eventService.ENTITY_DELETED, async ({ entityName, entity }) => {
 | 
				
			||||||
 | 
					    await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => {
 | 
				
			||||||
 | 
					        // if one mirror attribute is deleted then the other should be deleted as well
 | 
				
			||||||
 | 
					        const relations = await targetNote.getRelations(definition.mirrorRelation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const relation of relations) {
 | 
				
			||||||
 | 
					            relation.isDeleted = true;
 | 
				
			||||||
 | 
					            await relation.save();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (relations.length > 0) {
 | 
				
			||||||
 | 
					            targetNote.invalidateAttributeCache();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -96,20 +96,17 @@ async function updateEntity(entity) {
 | 
				
			|||||||
        if (entity.isChanged && (entityName !== 'options' || entity.isSynced)) {
 | 
					        if (entity.isChanged && (entityName !== 'options' || entity.isSynced)) {
 | 
				
			||||||
            await syncTableService.addEntitySync(entityName, primaryKey);
 | 
					            await syncTableService.addEntitySync(entityName, primaryKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (isNewEntity) {
 | 
					            const eventPayload = {
 | 
				
			||||||
                await eventService.emit(eventService.ENTITY_CREATED, {
 | 
					 | 
				
			||||||
                entityName,
 | 
					                entityName,
 | 
				
			||||||
                entity
 | 
					                entity
 | 
				
			||||||
                });
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (isNewEntity && !entity.isDeleted) {
 | 
				
			||||||
 | 
					                await eventService.emit(eventService.ENTITY_CREATED, eventPayload);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // it seems to be better to handle deletion with a separate event
 | 
					            // it seems to be better to handle deletion and update separately
 | 
				
			||||||
            if (!entity.isDeleted) {
 | 
					            await eventService.emit(entity.isDeleted ? eventService.ENTITY_DELETED : eventService.ENTITY_CHANGED, eventPayload);
 | 
				
			||||||
                await eventService.emit(eventService.ENTITY_CHANGED, {
 | 
					 | 
				
			||||||
                    entityName,
 | 
					 | 
				
			||||||
                    entity
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -68,6 +68,12 @@
 | 
				
			|||||||
                           data-bind="checked: relationDefinition.isPromoted"/>
 | 
					                           data-bind="checked: relationDefinition.isPromoted"/>
 | 
				
			||||||
                      Promoted
 | 
					                      Promoted
 | 
				
			||||||
                    </label>
 | 
					                    </label>
 | 
				
			||||||
 | 
					                    <br/>
 | 
				
			||||||
 | 
					                    <label>
 | 
				
			||||||
 | 
					                      Mirror relation:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                      <input type="text" value="true" class="attribute-name" data-bind="value: relationDefinition.mirrorRelation"/>
 | 
				
			||||||
 | 
					                    </label>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </td>
 | 
					                </td>
 | 
				
			||||||
                <td title="Inheritable relations are automatically inherited to the child notes">
 | 
					                <td title="Inheritable relations are automatically inherited to the child notes">
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user