mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	Compare commits
	
		
			24 Commits
		
	
	
		
			v0.24.0-be
			...
			v0.24.2-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9c834229b9 | ||
| 
						 | 
					3fd45b15e7 | ||
| 
						 | 
					f20ab45576 | ||
| 
						 | 
					77a89d85c8 | ||
| 
						 | 
					30249a353e | ||
| 
						 | 
					eb9bae9010 | ||
| 
						 | 
					0c7ae527c5 | ||
| 
						 | 
					fef4705e2f | ||
| 
						 | 
					568c2c997f | ||
| 
						 | 
					d6b5cd6ead | ||
| 
						 | 
					00ce379962 | ||
| 
						 | 
					b1ed022771 | ||
| 
						 | 
					ad6cb6ba34 | ||
| 
						 | 
					2e76de5f34 | ||
| 
						 | 
					4f23f2515a | ||
| 
						 | 
					8e16cc2326 | ||
| 
						 | 
					49bca04ebb | ||
| 
						 | 
					05e9669eaf | ||
| 
						 | 
					62a250a7fc | ||
| 
						 | 
					8299524682 | ||
| 
						 | 
					7691a59977 | ||
| 
						 | 
					3db2f6784d | ||
| 
						 | 
					48684d0509 | ||
| 
						 | 
					1ee8d9fd93 | 
@@ -1,4 +1,4 @@
 | 
				
			|||||||
FROM node:10.12.0
 | 
					FROM node:10.13.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN apt-get update && apt-get install -y nasm
 | 
					RUN apt-get update && apt-get install -y nasm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
VERSION=$1
 | 
					VERSION=$1
 | 
				
			||||||
PKG_DIR=dist/trilium-linux-x64-server
 | 
					PKG_DIR=dist/trilium-linux-x64-server
 | 
				
			||||||
NODE_VERSION=10.12.0
 | 
					NODE_VERSION=10.13.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
rm -r $PKG_DIR
 | 
					rm -r $PKG_DIR
 | 
				
			||||||
mkdir $PKG_DIR
 | 
					mkdir $PKG_DIR
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								bin/build.sh
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								bin/build.sh
									
									
									
									
									
								
							@@ -11,15 +11,21 @@ rm -r dist/*
 | 
				
			|||||||
echo "Rebuilding binaries for linux-ia32"
 | 
					echo "Rebuilding binaries for linux-ia32"
 | 
				
			||||||
./node_modules/.bin/electron-rebuild --arch=ia32
 | 
					./node_modules/.bin/electron-rebuild --arch=ia32
 | 
				
			||||||
 | 
					
 | 
				
			||||||
./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=ia32 --overwrite
 | 
					./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=linux --arch=ia32 --overwrite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
./node_modules/.bin/electron-packager . --out=dist --platform=win32 --arch=x64 --overwrite
 | 
					mv "./dist/Trilium Notes-linux-ia32" ./dist/trilium-linux-ia32
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=win32  --arch=x64 --overwrite --icon=src/public/images/app-icons/win/icon.ico
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mv "./dist/Trilium Notes-win32-x64" ./dist/trilium-win32-x64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# we build x64 as second so that we keep X64 binaries in node_modules for local development and server build
 | 
					# we build x64 as second so that we keep X64 binaries in node_modules for local development and server build
 | 
				
			||||||
echo "Rebuilding binaries for linux-x64"
 | 
					echo "Rebuilding binaries for linux-x64"
 | 
				
			||||||
./node_modules/.bin/electron-rebuild --arch=x64
 | 
					./node_modules/.bin/electron-rebuild --arch=x64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=x64 --overwrite
 | 
					./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=linux --arch=x64 --overwrite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mv "./dist/Trilium Notes-linux-x64" ./dist/trilium-linux-x64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo "Copying required windows binaries"
 | 
					echo "Copying required windows binaries"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								db/demo.tar
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db/demo.tar
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -1,3 +1,9 @@
 | 
				
			|||||||
 | 
					-- first fix deleted status of existing images
 | 
				
			||||||
 | 
					UPDATE note_images SET isDeleted = 1 WHERE noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- we don't need set data to null because table is going to be dropped anyway and we want image size into attribute
 | 
				
			||||||
 | 
					UPDATE images SET isDeleted = 1 WHERE imageId NOT IN (SELECT imageId FROM note_images WHERE isDeleted = 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- allow null for note content (for deleted notes)
 | 
					-- allow null for note content (for deleted notes)
 | 
				
			||||||
CREATE TABLE IF NOT EXISTS "notes_mig" (
 | 
					CREATE TABLE IF NOT EXISTS "notes_mig" (
 | 
				
			||||||
  `noteId`	TEXT NOT NULL,
 | 
					  `noteId`	TEXT NOT NULL,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								db/migrations/0117__fix_attributes_of_deleted_notes.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0117__fix_attributes_of_deleted_notes.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					UPDATE attributes SET isDeleted = 1 WHERE noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1);
 | 
				
			||||||
							
								
								
									
										1
									
								
								db/migrations/0118__fix_broken_relations.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0118__fix_broken_relations.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					UPDATE attributes SET isDeleted = 1 WHERE type = 'relation' AND value NOT IN (SELECT noteId FROM notes WHERE notes.isDeleted = 0);
 | 
				
			||||||
							
								
								
									
										1
									
								
								db/migrations/0119__rename_mirror_to_inverse.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0119__rename_mirror_to_inverse.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					UPDATE attributes SET value = replace(value, 'mirrorRelation', 'inverseRelation') WHERE type = 'relation-definition';
 | 
				
			||||||
@@ -70,6 +70,8 @@ app.on('activate', () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.on('ready', async () => {
 | 
					app.on('ready', async () => {
 | 
				
			||||||
 | 
					    app.setAppUserModelId('com.github.zadam.trilium');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mainWindow = await createMainWindow();
 | 
					    mainWindow = await createMainWindow();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const result = globalShortcut.register('CommandOrControl+Alt+P', cls.wrap(async () => {
 | 
					    const result = globalShortcut.register('CommandOrControl+Alt+P', cls.wrap(async () => {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "trilium",
 | 
					  "name": "trilium",
 | 
				
			||||||
  "version": "0.23.1",
 | 
					  "version": "0.24.1-beta",
 | 
				
			||||||
  "lockfileVersion": 1,
 | 
					  "lockfileVersion": 1,
 | 
				
			||||||
  "requires": true,
 | 
					  "requires": true,
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,8 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "trilium",
 | 
					  "name": "trilium",
 | 
				
			||||||
 | 
					  "productName": "Trilium Notes",
 | 
				
			||||||
  "description": "Trilium Notes",
 | 
					  "description": "Trilium Notes",
 | 
				
			||||||
  "version": "0.24.0-beta",
 | 
					  "version": "0.24.2-beta",
 | 
				
			||||||
  "license": "AGPL-3.0-only",
 | 
					  "license": "AGPL-3.0-only",
 | 
				
			||||||
  "main": "electron.js",
 | 
					  "main": "electron.js",
 | 
				
			||||||
  "bin": {
 | 
					  "bin": {
 | 
				
			||||||
@@ -13,10 +14,7 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "start": "node ./src/www",
 | 
					    "start": "node ./src/www",
 | 
				
			||||||
    "test-electron": "xo",
 | 
					 | 
				
			||||||
    "rebuild-electron": "electron-rebuild",
 | 
					 | 
				
			||||||
    "start-electron": "electron . --disable-gpu",
 | 
					    "start-electron": "electron . --disable-gpu",
 | 
				
			||||||
    "build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64 --app-version= --icon=src/public/app-icons/win/icon.ico",
 | 
					 | 
				
			||||||
    "build-backend-docs": "jsdoc -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
 | 
					    "build-backend-docs": "jsdoc -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
 | 
				
			||||||
    "build-frontend-docs": "jsdoc -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js",
 | 
					    "build-frontend-docs": "jsdoc -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js",
 | 
				
			||||||
    "build-docs": "npm run build-backend-docs && npm run build-frontend-docs"
 | 
					    "build-docs": "npm run build-backend-docs && npm run build-frontend-docs"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,7 +72,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: "",
 | 
					                inverseRelation: "",
 | 
				
			||||||
                isPromoted: true
 | 
					                isPromoted: true
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -191,7 +191,7 @@ function AttributesModel() {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
                relationDefinition: {
 | 
					                relationDefinition: {
 | 
				
			||||||
                    multiplicityType: "singlevalue",
 | 
					                    multiplicityType: "singlevalue",
 | 
				
			||||||
                    mirrorRelation: "",
 | 
					                    inverseRelation: "",
 | 
				
			||||||
                    isPromoted: true
 | 
					                    isPromoted: true
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }));
 | 
					            }));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,9 @@
 | 
				
			|||||||
const $dialog = $("#prompt-dialog");
 | 
					const $dialog = $("#prompt-dialog");
 | 
				
			||||||
const $question = $("#prompt-dialog-question");
 | 
					const $dialogBody = $dialog.find(".modal-body");
 | 
				
			||||||
const $answer = $("#prompt-dialog-answer");
 | 
					
 | 
				
			||||||
 | 
					let $question;
 | 
				
			||||||
 | 
					let $answer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const $form = $("#prompt-dialog-form");
 | 
					const $form = $("#prompt-dialog-form");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let resolve;
 | 
					let resolve;
 | 
				
			||||||
@@ -11,8 +14,21 @@ function ask({ message, defaultValue, shown }) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    shownCb = shown;
 | 
					    shownCb = shown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $question.text(message);
 | 
					    $question = $("<label>")
 | 
				
			||||||
    $answer.val(defaultValue || "");
 | 
					        .prop("for", "prompt-dialog-answer")
 | 
				
			||||||
 | 
					        .text(message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $answer = $("<input>")
 | 
				
			||||||
 | 
					        .prop("type", "text")
 | 
				
			||||||
 | 
					        .prop("id", "prompt-dialog-answer")
 | 
				
			||||||
 | 
					        .addClass("form-control")
 | 
				
			||||||
 | 
					        .val(defaultValue || "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $dialogBody.empty().append(
 | 
				
			||||||
 | 
					        $("<div>")
 | 
				
			||||||
 | 
					            .addClass("form-group")
 | 
				
			||||||
 | 
					            .append($question)
 | 
				
			||||||
 | 
					            .append($answer));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $dialog.modal();
 | 
					    $dialog.modal();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -50,7 +50,12 @@ async function execute(e) {
 | 
				
			|||||||
    e.preventDefault();
 | 
					    e.preventDefault();
 | 
				
			||||||
    e.stopPropagation();
 | 
					    e.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const sqlQuery = codeEditor.getValue();
 | 
					    // execute the selected text or the whole content if there's no selection
 | 
				
			||||||
 | 
					    let sqlQuery = codeEditor.getSelection();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!sqlQuery) {
 | 
				
			||||||
 | 
					        sqlQuery = codeEditor.getValue();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const result = await server.post("sql/execute", {
 | 
					    const result = await server.post("sql/execute", {
 | 
				
			||||||
        query: sqlQuery
 | 
					        query: sqlQuery
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							@@ -103,7 +103,13 @@ if (utils.isElectron()) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$("#export-note-to-markdown-button").click(() => exportService.exportSubtree(noteDetailService.getCurrentNoteId(), 'markdown-single'));
 | 
					$("#export-note-to-markdown-button").click(function () {
 | 
				
			||||||
 | 
					    if ($(this).hasClass("disabled")) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    exportService.exportSubtree(noteDetailService.getCurrentNoteId(), 'markdown-single')
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
treeService.showTree();
 | 
					treeService.showTree();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,15 +10,13 @@ const dragAndDropSetup = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        node.setSelected(true);
 | 
					        node.setSelected(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const selectedNodes = treeService.getSelectedNodes().map(node => {
 | 
					        // this is for dragging notes into relation map
 | 
				
			||||||
            return {
 | 
					        // we allow to drag only one note at a time because it multi-drag conflicts with multiple single drags
 | 
				
			||||||
 | 
					        // in UX and single drag is probably more useful
 | 
				
			||||||
 | 
					        data.dataTransfer.setData("text", JSON.stringify({
 | 
				
			||||||
            noteId: node.data.noteId,
 | 
					            noteId: node.data.noteId,
 | 
				
			||||||
            title: node.title
 | 
					            title: node.title
 | 
				
			||||||
            }
 | 
					        }));
 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // this is for dragging notes into relation map
 | 
					 | 
				
			||||||
        data.dataTransfer.setData("text", JSON.stringify(selectedNodes));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // This function MUST be defined to enable dragging for the tree.
 | 
					        // This function MUST be defined to enable dragging for the tree.
 | 
				
			||||||
        // Return false to cancel dragging of node.
 | 
					        // Return false to cancel dragging of node.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,9 +25,21 @@ function registerEntrypoints() {
 | 
				
			|||||||
    $("#jump-to-note-dialog-button").click(jumpToNoteDialog.showDialog);
 | 
					    $("#jump-to-note-dialog-button").click(jumpToNoteDialog.showDialog);
 | 
				
			||||||
    utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog);
 | 
					    utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions);
 | 
					    $("#show-note-revisions-button").click(function() {
 | 
				
			||||||
 | 
					        if ($(this).hasClass("disabled")) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $("#show-source-button").click(noteSourceDialog.showDialog);
 | 
					        noteRevisionsDialog.showCurrentNoteRevisions();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $("#show-source-button").click(function() {
 | 
				
			||||||
 | 
					        if ($(this).hasClass("disabled")) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        noteSourceDialog.showDialog();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $("#recent-changes-button").click(recentChangesDialog.showDialog);
 | 
					    $("#recent-changes-button").click(recentChangesDialog.showDialog);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import infoService from "./info.js";
 | 
				
			|||||||
import server from "./server.js";
 | 
					import server from "./server.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const $component = $('#note-detail-image');
 | 
					const $component = $('#note-detail-image');
 | 
				
			||||||
 | 
					const $imageWrapper = $('#note-detail-image-wrapper');
 | 
				
			||||||
const $imageView = $('#note-detail-image-view');
 | 
					const $imageView = $('#note-detail-image-view');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const $imageDownloadButton = $("#image-download");
 | 
					const $imageDownloadButton = $("#image-download");
 | 
				
			||||||
@@ -39,10 +40,10 @@ function selectImage(element) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$copyToClipboardButton.click(() => {
 | 
					$copyToClipboardButton.click(() => {
 | 
				
			||||||
    $component.attr('contenteditable','true');
 | 
					    $imageWrapper.attr('contenteditable','true');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        selectImage($component.get(0));
 | 
					        selectImage($imageWrapper.get(0));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const success = document.execCommand('copy');
 | 
					        const success = document.execCommand('copy');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -55,7 +56,7 @@ $copyToClipboardButton.click(() => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    finally {
 | 
					    finally {
 | 
				
			||||||
        window.getSelection().removeAllRanges();
 | 
					        window.getSelection().removeAllRanges();
 | 
				
			||||||
        $component.removeAttr('contenteditable');
 | 
					        $imageWrapper.removeAttr('contenteditable');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,7 @@ const $relationMapContainer = $("#relation-map-container");
 | 
				
			|||||||
const $createChildNote = $("#relation-map-create-child-note");
 | 
					const $createChildNote = $("#relation-map-create-child-note");
 | 
				
			||||||
const $zoomInButton = $("#relation-map-zoom-in");
 | 
					const $zoomInButton = $("#relation-map-zoom-in");
 | 
				
			||||||
const $zoomOutButton = $("#relation-map-zoom-out");
 | 
					const $zoomOutButton = $("#relation-map-zoom-out");
 | 
				
			||||||
const $centerButton = $("#relation-map-center");
 | 
					const $resetPanZoomButton = $("#relation-map-reset-pan-zoom");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let mapData;
 | 
					let mapData;
 | 
				
			||||||
let jsPlumbInstance;
 | 
					let jsPlumbInstance;
 | 
				
			||||||
@@ -50,7 +50,7 @@ const biDirectionalOverlays = [
 | 
				
			|||||||
    } ]
 | 
					    } ]
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mirrorOverlays = [
 | 
					const inverseRelationsOverlays = [
 | 
				
			||||||
    [ "Arrow", {
 | 
					    [ "Arrow", {
 | 
				
			||||||
        location: 1,
 | 
					        location: 1,
 | 
				
			||||||
        id: "arrow",
 | 
					        id: "arrow",
 | 
				
			||||||
@@ -117,6 +117,15 @@ async function show() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function clearMap() {
 | 
				
			||||||
 | 
					    // delete all endpoints and connections
 | 
				
			||||||
 | 
					    // this is done at this point (after async operations) to reduce flicker to the minimum
 | 
				
			||||||
 | 
					    jsPlumbInstance.deleteEveryEndpoint();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // without this we still end up with note boxes remaining in the canvas
 | 
				
			||||||
 | 
					    $relationMapContainer.empty();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function loadNotesAndRelations() {
 | 
					async function loadNotesAndRelations() {
 | 
				
			||||||
    const noteIds = mapData.notes.map(note => note.noteId);
 | 
					    const noteIds = mapData.notes.map(note => note.noteId);
 | 
				
			||||||
    const data = await server.post("notes/relation-map", {noteIds});
 | 
					    const data = await server.post("notes/relation-map", {noteIds});
 | 
				
			||||||
@@ -125,12 +134,12 @@ async function loadNotesAndRelations() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    for (const relation of data.relations) {
 | 
					    for (const relation of data.relations) {
 | 
				
			||||||
        const match = relations.find(rel =>
 | 
					        const match = relations.find(rel =>
 | 
				
			||||||
            rel.name === data.mirrorRelations[relation.name]
 | 
					            rel.name === data.inverseRelations[relation.name]
 | 
				
			||||||
            && ((rel.sourceNoteId === relation.sourceNoteId && rel.targetNoteId === relation.targetNoteId)
 | 
					            && ((rel.sourceNoteId === relation.sourceNoteId && rel.targetNoteId === relation.targetNoteId)
 | 
				
			||||||
            || (rel.sourceNoteId === relation.targetNoteId && rel.targetNoteId === relation.sourceNoteId)));
 | 
					            || (rel.sourceNoteId === relation.targetNoteId && rel.targetNoteId === relation.sourceNoteId)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (match) {
 | 
					        if (match) {
 | 
				
			||||||
            match.type = relation.type = relation.name === data.mirrorRelations[relation.name] ? 'biDirectional' : 'mirror';
 | 
					            match.type = relation.type = relation.name === data.inverseRelations[relation.name] ? 'biDirectional' : 'inverse';
 | 
				
			||||||
            relation.render = false; // don't render second relation
 | 
					            relation.render = false; // don't render second relation
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            relation.type = 'uniDirectional';
 | 
					            relation.type = 'uniDirectional';
 | 
				
			||||||
@@ -142,11 +151,9 @@ async function loadNotesAndRelations() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    mapData.notes = mapData.notes.filter(note => note.noteId in data.noteTitles);
 | 
					    mapData.notes = mapData.notes.filter(note => note.noteId in data.noteTitles);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // delete all endpoints and connections
 | 
					 | 
				
			||||||
    // this is done at this point (after async operations) to reduce flicker to the minimum
 | 
					 | 
				
			||||||
    jsPlumbInstance.deleteEveryEndpoint();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    jsPlumbInstance.batch(async function () {
 | 
					    jsPlumbInstance.batch(async function () {
 | 
				
			||||||
 | 
					        clearMap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const note of mapData.notes) {
 | 
					        for (const note of mapData.notes) {
 | 
				
			||||||
            const title = data.noteTitles[note.noteId];
 | 
					            const title = data.noteTitles[note.noteId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -166,9 +173,9 @@ async function loadNotesAndRelations() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            connection.id = relation.attributeId;
 | 
					            connection.id = relation.attributeId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (relation.type === 'mirror') {
 | 
					            if (relation.type === 'inverse') {
 | 
				
			||||||
                connection.getOverlay("label-source").setLabel(relation.name);
 | 
					                connection.getOverlay("label-source").setLabel(relation.name);
 | 
				
			||||||
                connection.getOverlay("label-target").setLabel(data.mirrorRelations[relation.name]);
 | 
					                connection.getOverlay("label-target").setLabel(data.inverseRelations[relation.name]);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else {
 | 
					            else {
 | 
				
			||||||
                connection.getOverlay("label").setLabel(relation.name);
 | 
					                connection.getOverlay("label").setLabel(relation.name);
 | 
				
			||||||
@@ -208,10 +215,17 @@ function initPanZoom() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                mapData.notes.push({ noteId: clipboard.noteId, x, y });
 | 
					                mapData.notes.push({ noteId: clipboard.noteId, x, y });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                saveData();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                clipboard = null;
 | 
					                clipboard = null;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return true;
 | 
					            return true;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        filterKey: function(e, dx, dy, dz) {
 | 
				
			||||||
 | 
					            // if ALT is pressed then panzoom should bubble the event up
 | 
				
			||||||
 | 
					            // this is to preserve ALT-LEFT, ALT-RIGHT navigation working
 | 
				
			||||||
 | 
					            return e.altKey;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -226,6 +240,10 @@ function initPanZoom() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        pzInstance.moveTo(mapData.transform.x, mapData.transform.y);
 | 
					        pzInstance.moveTo(mapData.transform.x, mapData.transform.y);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					        // set to initial coordinates
 | 
				
			||||||
 | 
					        pzInstance.moveTo(0, 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $zoomInButton.click(() => pzInstance.zoomTo(0, 0, 1.2));
 | 
					    $zoomInButton.click(() => pzInstance.zoomTo(0, 0, 1.2));
 | 
				
			||||||
    $zoomOutButton.click(() => pzInstance.zoomTo(0, 0, 0.8));
 | 
					    $zoomOutButton.click(() => pzInstance.zoomTo(0, 0, 0.8));
 | 
				
			||||||
@@ -244,11 +262,7 @@ function saveCurrentTransform() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function cleanup() {
 | 
					function cleanup() {
 | 
				
			||||||
    if (jsPlumbInstance) {
 | 
					    if (jsPlumbInstance) {
 | 
				
			||||||
        // delete all endpoints and connections
 | 
					        clearMap();
 | 
				
			||||||
        jsPlumbInstance.deleteEveryEndpoint();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // without this we still end up with note boxes remaining in the canvas
 | 
					 | 
				
			||||||
        $relationMapContainer.empty();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (pzInstance) {
 | 
					    if (pzInstance) {
 | 
				
			||||||
@@ -276,7 +290,7 @@ function initJsPlumbInstance () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    jsPlumbInstance.registerConnectionType("biDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: biDirectionalOverlays });
 | 
					    jsPlumbInstance.registerConnectionType("biDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: biDirectionalOverlays });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    jsPlumbInstance.registerConnectionType("mirror", { anchor:"Continuous", connector:"StateMachine", overlays: mirrorOverlays });
 | 
					    jsPlumbInstance.registerConnectionType("inverse", { anchor:"Continuous", connector:"StateMachine", overlays: inverseRelationsOverlays });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    jsPlumbInstance.registerConnectionType("link", { anchor:"Continuous", connector:"StateMachine", overlays: linkOverlays });
 | 
					    jsPlumbInstance.registerConnectionType("link", { anchor:"Continuous", connector:"StateMachine", overlays: linkOverlays });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -312,8 +326,6 @@ function connectionContextMenuHandler(connection, event) {
 | 
				
			|||||||
async function connectionCreatedHandler(info, originalEvent) {
 | 
					async function connectionCreatedHandler(info, originalEvent) {
 | 
				
			||||||
    const connection = info.connection;
 | 
					    const connection = info.connection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const isRelation = relations.some(rel => rel.attributeId === connection.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    connection.bind("contextmenu", (obj, event) => {
 | 
					    connection.bind("contextmenu", (obj, event) => {
 | 
				
			||||||
        if (connection.getType().includes("link")) {
 | 
					        if (connection.getType().includes("link")) {
 | 
				
			||||||
            // don't create context menu if it's a link since there's nothing to do with link from relation map
 | 
					            // don't create context menu if it's a link since there's nothing to do with link from relation map
 | 
				
			||||||
@@ -362,9 +374,7 @@ async function connectionCreatedHandler(info, originalEvent) {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const attribute = await server.put(`notes/${sourceNoteId}/relations/${name}/to/${targetNoteId}`);
 | 
					    await server.put(`notes/${sourceNoteId}/relations/${name}/to/${targetNoteId}`);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    relations.push({ attributeId: attribute.attributeId , targetNoteId, sourceNoteId, name });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await refresh();
 | 
					    await refresh();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -512,43 +522,20 @@ function getZoom() {
 | 
				
			|||||||
async function dropNoteOntoRelationMapHandler(ev) {
 | 
					async function dropNoteOntoRelationMapHandler(ev) {
 | 
				
			||||||
    ev.preventDefault();
 | 
					    ev.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const notes = JSON.parse(ev.originalEvent.dataTransfer.getData("text"));
 | 
					    const note = JSON.parse(ev.originalEvent.dataTransfer.getData("text"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let {x, y} = getMousePosition(ev);
 | 
					    let {x, y} = getMousePosition(ev);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // modifying position so that cursor is on the top-center of the box
 | 
					 | 
				
			||||||
    const startX = x -= 80;
 | 
					 | 
				
			||||||
    y -= 15;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const currentNoteId = treeService.getCurrentNode().data.noteId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const note of notes) {
 | 
					 | 
				
			||||||
        if (note.noteId === currentNoteId) {
 | 
					 | 
				
			||||||
            // we don't allow placing current (relation map) into itself
 | 
					 | 
				
			||||||
            // the reason is that when dragging notes from the tree, the relation map is always selected
 | 
					 | 
				
			||||||
            // since it's focused.
 | 
					 | 
				
			||||||
            continue;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const exists = mapData.notes.some(n => n.noteId === note.noteId);
 | 
					    const exists = mapData.notes.some(n => n.noteId === note.noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (exists) {
 | 
					    if (exists) {
 | 
				
			||||||
        await infoDialog.info(`Note "${note.title}" is already placed into the diagram`);
 | 
					        await infoDialog.info(`Note "${note.title}" is already placed into the diagram`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            continue;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mapData.notes.push({noteId: note.noteId, x, y});
 | 
					    mapData.notes.push({noteId: note.noteId, x, y});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (x - startX > 1000) {
 | 
					 | 
				
			||||||
            x = startX;
 | 
					 | 
				
			||||||
            y += 200;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        else {
 | 
					 | 
				
			||||||
            x += 200;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    saveData();
 | 
					    saveData();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await refresh();
 | 
					    await refresh();
 | 
				
			||||||
@@ -565,40 +552,10 @@ function getMousePosition(evt) {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$centerButton.click(() => {
 | 
					$resetPanZoomButton.click(() => {
 | 
				
			||||||
    if (mapData.notes.length === 0) {
 | 
					    // reset to initial pan & zoom state
 | 
				
			||||||
        return; // nothing to recenter on
 | 
					    pzInstance.zoomTo(0, 0, 1 / getZoom());
 | 
				
			||||||
    }
 | 
					    pzInstance.moveTo(0, 0);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    let totalX = 0, totalY = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const note of mapData.notes) {
 | 
					 | 
				
			||||||
        totalX += note.x;
 | 
					 | 
				
			||||||
        totalY += note.y;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let averageX = totalX / mapData.notes.length;
 | 
					 | 
				
			||||||
    let averageY = totalY / mapData.notes.length;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // find note with smallest X, Y difference from the average (most central note)
 | 
					 | 
				
			||||||
    const {noteId} = mapData.notes.map(note => {
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            noteId: note.noteId,
 | 
					 | 
				
			||||||
            diff: Math.abs(note.x - averageX) + Math.abs(note.y - averageY)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }).reduce((min, val) => min.diff <= val.min ? min : val, { diff: 9999999999 });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const $noteBox = $("#" + noteIdToId(noteId));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const clientRect = $noteBox[0].getBoundingClientRect();
 | 
					 | 
				
			||||||
    const cx = clientRect.left + clientRect.width / 2;
 | 
					 | 
				
			||||||
    const cy = clientRect.top + clientRect.height / 2;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const container = $component[0].getBoundingClientRect();
 | 
					 | 
				
			||||||
    const dx = container.width / 2 - cx;
 | 
					 | 
				
			||||||
    const dy = container.height / 2 - cy;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pzInstance.moveBy(dx, dy, true);
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$component.on("drop", dropNoteOntoRelationMapHandler);
 | 
					$component.on("drop", dropNoteOntoRelationMapHandler);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,6 +41,10 @@ function setupTooltip() {
 | 
				
			|||||||
        if ($(this).is(":hover")) {
 | 
					        if ($(this).is(":hover")) {
 | 
				
			||||||
            $(this).tooltip({
 | 
					            $(this).tooltip({
 | 
				
			||||||
                delay: {"show": 300, "hide": 100},
 | 
					                delay: {"show": 300, "hide": 100},
 | 
				
			||||||
 | 
					                container: 'body',
 | 
				
			||||||
 | 
					                placement: 'auto',
 | 
				
			||||||
 | 
					                trigger: 'manual',
 | 
				
			||||||
 | 
					                boundariesElement: 'window',
 | 
				
			||||||
                title: html,
 | 
					                title: html,
 | 
				
			||||||
                html: true
 | 
					                html: true
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@@ -50,7 +54,7 @@ function setupTooltip() {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $(document).on("mouseleave", "a", function() {
 | 
					    $(document).on("mouseleave", "a", function() {
 | 
				
			||||||
        $(this).tooltip('hide');
 | 
					        $(this).tooltip('dispose');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // close any tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
 | 
					    // close any tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,9 +5,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#relation-map-wrapper {
 | 
					#relation-map-wrapper {
 | 
				
			||||||
    position: relative;
 | 
					    position: relative;
 | 
				
			||||||
    overflow: hidden !important;
 | 
					    height: 100%;
 | 
				
			||||||
    height: 4000px; /* we need to set fixed dimentions. This number is probably enough to cover any screen */
 | 
					 | 
				
			||||||
    width: 4000px;
 | 
					 | 
				
			||||||
    outline: none; /* remove dotted outline on click */
 | 
					    outline: none; /* remove dotted outline on click */
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -76,6 +76,7 @@ body {
 | 
				
			|||||||
    position: relative;
 | 
					    position: relative;
 | 
				
			||||||
    overflow: auto;
 | 
					    overflow: auto;
 | 
				
			||||||
    flex-basis: content;
 | 
					    flex-basis: content;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.note-detail-component {
 | 
					.note-detail-component {
 | 
				
			||||||
@@ -557,6 +558,10 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
 | 
				
			|||||||
    max-height: 250px;
 | 
					    max-height: 250px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tooltip-inner figure.image-style-side {
 | 
				
			||||||
 | 
					    float: right;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tooltip.show {
 | 
					.tooltip.show {
 | 
				
			||||||
    opacity: 1;
 | 
					    opacity: 1;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -95,7 +95,7 @@ async function updateNoteAttributes(req) {
 | 
				
			|||||||
        attributeEntity.isInheritable = attribute.isInheritable;
 | 
					        attributeEntity.isInheritable = attribute.isInheritable;
 | 
				
			||||||
        attributeEntity.isDeleted = attribute.isDeleted;
 | 
					        attributeEntity.isDeleted = attribute.isDeleted;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (attributeEntity.type === 'relation' && !attributeEntity.value.trim()) {
 | 
					        if (attributeEntity.type === 'relation' && !attribute.value.trim()) {
 | 
				
			||||||
            // relation should never have empty target
 | 
					            // relation should never have empty target
 | 
				
			||||||
            attributeEntity.isDeleted = true;
 | 
					            attributeEntity.isDeleted = true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -117,8 +117,8 @@ async function getRelationMap(req) {
 | 
				
			|||||||
        // noteId => title
 | 
					        // noteId => title
 | 
				
			||||||
        noteTitles: {},
 | 
					        noteTitles: {},
 | 
				
			||||||
        relations: [],
 | 
					        relations: [],
 | 
				
			||||||
        // relation name => mirror relation name
 | 
					        // relation name => inverse relation name
 | 
				
			||||||
        mirrorRelations: {},
 | 
					        inverseRelations: {},
 | 
				
			||||||
        links: []
 | 
					        links: []
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -143,8 +143,8 @@ async function getRelationMap(req) {
 | 
				
			|||||||
            }; }));
 | 
					            }; }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const relationDefinition of await note.getRelationDefinitions()) {
 | 
					        for (const relationDefinition of await note.getRelationDefinitions()) {
 | 
				
			||||||
            if (relationDefinition.value.mirrorRelation) {
 | 
					            if (relationDefinition.value.inverseRelation) {
 | 
				
			||||||
                resp.mirrorRelations[relationDefinition.name] = relationDefinition.value.mirrorRelation;
 | 
					                resp.inverseRelations[relationDefinition.name] = relationDefinition.value.inverseRelation;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
const build = require('./build');
 | 
					const build = require('./build');
 | 
				
			||||||
const packageJson = require('../../package');
 | 
					const packageJson = require('../../package');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const APP_DB_VERSION = 116;
 | 
					const APP_DB_VERSION = 119;
 | 
				
			||||||
const SYNC_VERSION = 2;
 | 
					const SYNC_VERSION = 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1 @@
 | 
				
			|||||||
module.exports = { buildDate:"2018-11-16T23:30:52+01:00", buildRevision: "90eb1b53fbe915c4658617772aea4347a107a722" };
 | 
					module.exports = { buildDate:"2018-11-19T17:17:08+01:00", buildRevision: "3fd45b15e7042c12f140524297b50677f9851044" };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -275,8 +275,9 @@ async function runAllChecks() {
 | 
				
			|||||||
            LEFT JOIN notes AS sourceNote ON sourceNote.noteId = links.noteId AND sourceNote.isDeleted = 0
 | 
					            LEFT JOIN notes AS sourceNote ON sourceNote.noteId = links.noteId AND sourceNote.isDeleted = 0
 | 
				
			||||||
            LEFT JOIN notes AS targetNote ON targetNote.noteId = links.noteId AND targetNote.isDeleted = 0
 | 
					            LEFT JOIN notes AS targetNote ON targetNote.noteId = links.noteId AND targetNote.isDeleted = 0
 | 
				
			||||||
          WHERE 
 | 
					          WHERE 
 | 
				
			||||||
            sourceNote.noteId IS NULL
 | 
					            links.isDeleted = 0
 | 
				
			||||||
            OR targetNote.noteId IS NULL`,
 | 
					            AND (sourceNote.noteId IS NULL
 | 
				
			||||||
 | 
					                 OR targetNote.noteId IS NULL)`,
 | 
				
			||||||
        "Link to source/target note link is broken", errorList);
 | 
					        "Link to source/target note link is broken", errorList);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await runSyncRowChecks("notes", "noteId", errorList);
 | 
					    await runSyncRowChecks("notes", "noteId", errorList);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,8 +4,20 @@ const sanitize = require("sanitize-filename");
 | 
				
			|||||||
const TurndownService = require('turndown');
 | 
					const TurndownService = require('turndown');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function exportSingleMarkdown(note, res) {
 | 
					async function exportSingleMarkdown(note, res) {
 | 
				
			||||||
 | 
					    if (note.type !== 'text' && note.type !== 'code') {
 | 
				
			||||||
 | 
					        return [400, `Note type ${note.type} cannot be exported as single markdown file.`];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let markdown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (note.type === 'code') {
 | 
				
			||||||
 | 
					        markdown = '```\n' + note.content + "\n```";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else if (note.type === 'text') {
 | 
				
			||||||
        const turndownService = new TurndownService();
 | 
					        const turndownService = new TurndownService();
 | 
				
			||||||
    const markdown = turndownService.turndown(note.content);
 | 
					        markdown = turndownService.turndown(note.content);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const name = sanitize(note.title);
 | 
					    const name = sanitize(note.title);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.setHeader('Content-Disposition', 'file; filename="' + name + '.md"');
 | 
					    res.setHeader('Content-Disposition', 'file; filename="' + name + '.md"');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@ async function exportToMarkdown(branch, res) {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        saveDataFile(childFileName, note);
 | 
					        saveNote(childFileName, note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const childNotes = await note.getChildNotes();
 | 
					        const childNotes = await note.getChildNotes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -40,11 +40,7 @@ async function exportToMarkdown(branch, res) {
 | 
				
			|||||||
        return childFileName;
 | 
					        return childFileName;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function saveDataFile(childFileName, note) {
 | 
					    function saveTextNote(childFileName, note) {
 | 
				
			||||||
        if (note.type !== 'text' && note.type !== 'code') {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (note.content.trim().length === 0) {
 | 
					        if (note.content.trim().length === 0) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -65,6 +61,19 @@ async function exportToMarkdown(branch, res) {
 | 
				
			|||||||
        pack.entry({name: childFileName + ".md", size: markdown.length}, markdown);
 | 
					        pack.entry({name: childFileName + ".md", size: markdown.length}, markdown);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function saveFileNote(childFileName, note) {
 | 
				
			||||||
 | 
					        pack.entry({name: childFileName, size: note.content.length}, note.content);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function saveNote(childFileName, note) {
 | 
				
			||||||
 | 
					        if (note.type === 'text' || note.type === 'code') {
 | 
				
			||||||
 | 
					            saveTextNote(childFileName, note);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if (note.type === 'image' || note.type === 'file') {
 | 
				
			||||||
 | 
					            saveFileNote(childFileName, note);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function saveDirectory(childFileName) {
 | 
					    function saveDirectory(childFileName) {
 | 
				
			||||||
        pack.entry({name: childFileName, type: 'directory'});
 | 
					        pack.entry({name: childFileName, type: 'directory'});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,11 @@ async function exportToOpml(branch, res) {
 | 
				
			|||||||
    async function exportNoteInner(branchId) {
 | 
					    async function exportNoteInner(branchId) {
 | 
				
			||||||
        const branch = await repository.getBranch(branchId);
 | 
					        const branch = await repository.getBranch(branchId);
 | 
				
			||||||
        const note = await branch.getNote();
 | 
					        const note = await branch.getNote();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (await note.hasLabel('excludeFromExport')) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
 | 
					        const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const preparedTitle = prepareText(title);
 | 
					        const preparedTitle = prepareText(title);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,7 +59,7 @@ eventService.subscribe(eventService.CHILD_NOTE_CREATED, async ({ parentNote, chi
 | 
				
			|||||||
    await runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote);
 | 
					    await runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function processMirrorRelations(entityName, entity, handler) {
 | 
					async function processInverseRelations(entityName, entity, handler) {
 | 
				
			||||||
    if (entityName === 'attributes' && entity.type === 'relation') {
 | 
					    if (entityName === 'attributes' && entity.type === 'relation') {
 | 
				
			||||||
        const note = await entity.getNote();
 | 
					        const note = await entity.getNote();
 | 
				
			||||||
        const attributes = (await note.getAttributes(entity.name)).filter(relation => relation.type === 'relation-definition');
 | 
					        const attributes = (await note.getAttributes(entity.name)).filter(relation => relation.type === 'relation-definition');
 | 
				
			||||||
@@ -67,7 +67,7 @@ async function processMirrorRelations(entityName, entity, handler) {
 | 
				
			|||||||
        for (const attribute of attributes) {
 | 
					        for (const attribute of attributes) {
 | 
				
			||||||
            const definition = attribute.value;
 | 
					            const definition = attribute.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (definition.mirrorRelation && definition.mirrorRelation.trim()) {
 | 
					            if (definition.inverseRelation && definition.inverseRelation.trim()) {
 | 
				
			||||||
                const targetNote = await entity.getTargetNote();
 | 
					                const targetNote = await entity.getTargetNote();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                await handler(definition, note, targetNote);
 | 
					                await handler(definition, note, targetNote);
 | 
				
			||||||
@@ -77,13 +77,17 @@ async function processMirrorRelations(entityName, entity, handler) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => {
 | 
					eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => {
 | 
				
			||||||
    await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => {
 | 
					    await processInverseRelations(entityName, entity, async (definition, note, targetNote) => {
 | 
				
			||||||
        // we need to make sure that also target's mirror attribute exists and if note, then create it
 | 
					        // we need to make sure that also target's inverse attribute exists and if note, then create it
 | 
				
			||||||
        if (!await targetNote.hasRelation(definition.mirrorRelation)) {
 | 
					        // inverse attribute has to target our note as well
 | 
				
			||||||
 | 
					        const hasInverseAttribute = (await targetNote.getRelations(definition.inverseRelation))
 | 
				
			||||||
 | 
					            .some(attr => attr.value === note.noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!hasInverseAttribute) {
 | 
				
			||||||
            await new Attribute({
 | 
					            await new Attribute({
 | 
				
			||||||
                noteId: targetNote.noteId,
 | 
					                noteId: targetNote.noteId,
 | 
				
			||||||
                type: 'relation',
 | 
					                type: 'relation',
 | 
				
			||||||
                name: definition.mirrorRelation,
 | 
					                name: definition.inverseRelation,
 | 
				
			||||||
                value: note.noteId,
 | 
					                value: note.noteId,
 | 
				
			||||||
                isInheritable: entity.isInheritable
 | 
					                isInheritable: entity.isInheritable
 | 
				
			||||||
            }).save();
 | 
					            }).save();
 | 
				
			||||||
@@ -94,16 +98,21 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
eventService.subscribe(eventService.ENTITY_DELETED, async ({ entityName, entity }) => {
 | 
					eventService.subscribe(eventService.ENTITY_DELETED, async ({ entityName, entity }) => {
 | 
				
			||||||
    await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => {
 | 
					    await processInverseRelations(entityName, entity, async (definition, note, targetNote) => {
 | 
				
			||||||
        // if one mirror attribute is deleted then the other should be deleted as well
 | 
					        // if one inverse attribute is deleted then the other should be deleted as well
 | 
				
			||||||
        const relations = await targetNote.getRelations(definition.mirrorRelation);
 | 
					        const relations = await targetNote.getRelations(definition.inverseRelation);
 | 
				
			||||||
 | 
					        let deletedSomething = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const relation of relations) {
 | 
					        for (const relation of relations) {
 | 
				
			||||||
 | 
					            if (relation.value === note.noteId) {
 | 
				
			||||||
                relation.isDeleted = true;
 | 
					                relation.isDeleted = true;
 | 
				
			||||||
                await relation.save();
 | 
					                await relation.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                deletedSomething = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (relations.length > 0) {
 | 
					        if (deletedSomething) {
 | 
				
			||||||
            targetNote.invalidateAttributeCache();
 | 
					            targetNote.invalidateAttributeCache();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,6 +31,11 @@ async function importTar(fileBuffer, parentNote) {
 | 
				
			|||||||
            return "";
 | 
					            return "";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // we allow references to root and they don't need translation
 | 
				
			||||||
 | 
					        if (origNoteId === 'root') {
 | 
				
			||||||
 | 
					            return origNoteId;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!ctx.noteIdMap[origNoteId]) {
 | 
					        if (!ctx.noteIdMap[origNoteId]) {
 | 
				
			||||||
            ctx.noteIdMap[origNoteId] = utils.newEntityId();
 | 
					            ctx.noteIdMap[origNoteId] = utils.newEntityId();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ const sqlInit = require('./sql_init');
 | 
				
			|||||||
const optionService = require('./options');
 | 
					const optionService = require('./options');
 | 
				
			||||||
const fs = require('fs-extra');
 | 
					const fs = require('fs-extra');
 | 
				
			||||||
const log = require('./log');
 | 
					const log = require('./log');
 | 
				
			||||||
 | 
					const utils = require('./utils');
 | 
				
			||||||
const resourceDir = require('./resource_dir');
 | 
					const resourceDir = require('./resource_dir');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function migrate() {
 | 
					async function migrate() {
 | 
				
			||||||
@@ -72,7 +73,7 @@ async function migrate() {
 | 
				
			|||||||
            log.error("error during migration to version " + mig.dbVersion + ": " + e.stack);
 | 
					            log.error("error during migration to version " + mig.dbVersion + ": " + e.stack);
 | 
				
			||||||
            log.error("migration failed, crashing hard"); // this is not very user friendly :-/
 | 
					            log.error("migration failed, crashing hard"); // this is not very user friendly :-/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            process.exit(1);
 | 
					            utils.crash();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        finally {
 | 
					        finally {
 | 
				
			||||||
            // make sure foreign keys are enabled even if migration script disables them
 | 
					            // make sure foreign keys are enabled even if migration script disables them
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,6 +68,10 @@ async function createNewNote(parentNoteId, noteData) {
 | 
				
			|||||||
    noteData.type = noteData.type || parentNote.type;
 | 
					    noteData.type = noteData.type || parentNote.type;
 | 
				
			||||||
    noteData.mime = noteData.mime || parentNote.mime;
 | 
					    noteData.mime = noteData.mime || parentNote.mime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (noteData.type === 'text' || noteData.type === 'code') {
 | 
				
			||||||
 | 
					        noteData.content = noteData.content || "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const note = await new Note({
 | 
					    const note = await new Note({
 | 
				
			||||||
        noteId: noteData.noteId, // optionally can force specific noteId
 | 
					        noteId: noteData.noteId, // optionally can force specific noteId
 | 
				
			||||||
        title: noteData.title,
 | 
					        title: noteData.title,
 | 
				
			||||||
@@ -173,7 +177,7 @@ async function protectNoteRevisions(note) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function findImageLinks(content, foundLinks) {
 | 
					function findImageLinks(content, foundLinks) {
 | 
				
			||||||
    const re = /src="\/api\/images\/([a-zA-Z0-9]+)\//g;
 | 
					    const re = /src="[^"]*\/api\/images\/([a-zA-Z0-9]+)\//g;
 | 
				
			||||||
    let match;
 | 
					    let match;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    while (match = re.exec(content)) {
 | 
					    while (match = re.exec(content)) {
 | 
				
			||||||
@@ -182,11 +186,13 @@ function findImageLinks(content, foundLinks) {
 | 
				
			|||||||
            targetNoteId: match[1]
 | 
					            targetNoteId: match[1]
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return match;
 | 
					
 | 
				
			||||||
 | 
					    // removing absolute references to server to keep it working between instances
 | 
				
			||||||
 | 
					    return content.replace(/src="[^"]*\/api\/images\//g, 'src="/api/images/');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function findHyperLinks(content, foundLinks) {
 | 
					function findHyperLinks(content, foundLinks) {
 | 
				
			||||||
    const re = /href="#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g;
 | 
					    const re = /href="[^"]*#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g;
 | 
				
			||||||
    let match;
 | 
					    let match;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    while (match = re.exec(content)) {
 | 
					    while (match = re.exec(content)) {
 | 
				
			||||||
@@ -196,7 +202,8 @@ function findHyperLinks(content, foundLinks) {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return match;
 | 
					    // removing absolute references to server to keep it working between instances
 | 
				
			||||||
 | 
					    return content.replace(/href="[^"]*#root/g, 'href="#root');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function findRelationMapLinks(content, foundLinks) {
 | 
					function findRelationMapLinks(content, foundLinks) {
 | 
				
			||||||
@@ -210,7 +217,7 @@ function findRelationMapLinks(content, foundLinks) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function saveLinks(note) {
 | 
					async function saveLinks(note, content) {
 | 
				
			||||||
    if (note.type !== 'text' && note.type !== 'relation-map') {
 | 
					    if (note.type !== 'text' && note.type !== 'relation-map') {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -218,11 +225,11 @@ async function saveLinks(note) {
 | 
				
			|||||||
    const foundLinks = [];
 | 
					    const foundLinks = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (note.type === 'text') {
 | 
					    if (note.type === 'text') {
 | 
				
			||||||
        findImageLinks(note.content, foundLinks);
 | 
					        content = findImageLinks(content, foundLinks);
 | 
				
			||||||
        findHyperLinks(note.content, foundLinks);
 | 
					        content = findHyperLinks(content, foundLinks);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (note.type === 'relation-map') {
 | 
					    else if (note.type === 'relation-map') {
 | 
				
			||||||
        findRelationMapLinks(note.content, foundLinks);
 | 
					        findRelationMapLinks(content, foundLinks);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else {
 | 
					    else {
 | 
				
			||||||
        throw new Error("Unrecognized type " + note.type);
 | 
					        throw new Error("Unrecognized type " + note.type);
 | 
				
			||||||
@@ -258,6 +265,8 @@ async function saveLinks(note) {
 | 
				
			|||||||
        unusedLink.isDeleted = true;
 | 
					        unusedLink.isDeleted = true;
 | 
				
			||||||
        await unusedLink.save();
 | 
					        await unusedLink.save();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return content;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function saveNoteRevision(note) {
 | 
					async function saveNoteRevision(note) {
 | 
				
			||||||
@@ -306,6 +315,8 @@ async function updateNote(noteId, noteUpdates) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const noteTitleChanged = note.title !== noteUpdates.title;
 | 
					    const noteTitleChanged = note.title !== noteUpdates.title;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    noteUpdates.content = await saveLinks(note, noteUpdates.content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    note.title = noteUpdates.title;
 | 
					    note.title = noteUpdates.title;
 | 
				
			||||||
    note.setContent(noteUpdates.content);
 | 
					    note.setContent(noteUpdates.content);
 | 
				
			||||||
    note.isProtected = noteUpdates.isProtected;
 | 
					    note.isProtected = noteUpdates.isProtected;
 | 
				
			||||||
@@ -315,8 +326,6 @@ async function updateNote(noteId, noteUpdates) {
 | 
				
			|||||||
        await triggerNoteTitleChanged(note);
 | 
					        await triggerNoteTitleChanged(note);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await saveLinks(note);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await protectNoteRevisions(note);
 | 
					    await protectNoteRevisions(note);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -374,19 +383,12 @@ async function deleteNote(branch) {
 | 
				
			|||||||
async function cleanupDeletedNotes() {
 | 
					async function cleanupDeletedNotes() {
 | 
				
			||||||
    const cutoffDate = new Date(new Date().getTime() - 48 * 3600 * 1000);
 | 
					    const cutoffDate = new Date(new Date().getTime() - 48 * 3600 * 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const notesForCleanup = await repository.getEntities("SELECT * FROM notes WHERE isDeleted = 1 AND content != '' AND dateModified <= ?", [dateUtils.dateStr(cutoffDate)]);
 | 
					    // it's better to not use repository for this because it will complain about saving protected notes
 | 
				
			||||||
 | 
					    // out of protected session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const note of notesForCleanup) {
 | 
					    await sql.execute("UPDATE notes SET content = NULL WHERE isDeleted = 1 AND content IS NOT NULL AND dateModified <= ?", [dateUtils.dateStr(cutoffDate)]);
 | 
				
			||||||
        note.content = null;
 | 
					 | 
				
			||||||
        await note.save();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const notesRevisionsForCleanup = await repository.getEntities("SELECT note_revisions.* FROM notes JOIN note_revisions USING(noteId) WHERE notes.isDeleted = 1 AND note_revisions.content != '' AND notes.dateModified <= ?", [dateUtils.dateStr(cutoffDate)]);
 | 
					    await sql.execute("UPDATE note_revisions SET content = NULL WHERE note_revisions.content IS NOT NULL AND noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1 AND notes.dateModified <= ?)", [dateUtils.dateStr(cutoffDate)]);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const noteRevision of notesRevisionsForCleanup) {
 | 
					 | 
				
			||||||
        noteRevision.content = null;
 | 
					 | 
				
			||||||
        await noteRevision.save();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// first cleanup kickoff 5 minutes after startup
 | 
					// first cleanup kickoff 5 minutes after startup
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -118,6 +118,15 @@ function escapeRegExp(str) {
 | 
				
			|||||||
    return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
 | 
					    return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function crash() {
 | 
				
			||||||
 | 
					    if (isElectron()) {
 | 
				
			||||||
 | 
					        require('electron').app.exit(1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					        process.exit(1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    randomSecureToken,
 | 
					    randomSecureToken,
 | 
				
			||||||
    randomString,
 | 
					    randomString,
 | 
				
			||||||
@@ -137,5 +146,6 @@ module.exports = {
 | 
				
			|||||||
    stripTags,
 | 
					    stripTags,
 | 
				
			||||||
    intersection,
 | 
					    intersection,
 | 
				
			||||||
    union,
 | 
					    union,
 | 
				
			||||||
    escapeRegExp
 | 
					    escapeRegExp,
 | 
				
			||||||
 | 
					    crash
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -22,5 +22,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <br/><br/>
 | 
					    <br/><br/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div id="note-detail-image-wrapper">
 | 
				
			||||||
        <img id="note-detail-image-view" />
 | 
					        <img id="note-detail-image-view" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
@@ -7,9 +7,9 @@
 | 
				
			|||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <button type="button"
 | 
					    <button type="button"
 | 
				
			||||||
            class="btn icon-button floating-button jam jam-align-center"
 | 
					            class="btn icon-button floating-button jam jam-crop"
 | 
				
			||||||
            title="Re-center view on notes"
 | 
					            title="Reset pan & zoom to initial coordinates and magnification"
 | 
				
			||||||
            id="relation-map-center" style="right: 100px;"></button>
 | 
					            id="relation-map-reset-pan-zoom" style="right: 100px;"></button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="btn-group floating-button" style="right: 20px;">
 | 
					    <div class="btn-group floating-button" style="right: 20px;">
 | 
				
			||||||
        <button type="button"
 | 
					        <button type="button"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,9 +72,9 @@
 | 
				
			|||||||
                    </label>
 | 
					                    </label>
 | 
				
			||||||
                    <br/>
 | 
					                    <br/>
 | 
				
			||||||
                    <label>
 | 
					                    <label>
 | 
				
			||||||
                      Mirror relation:
 | 
					                      Inverse relation:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                      <input type="text" value="true" class="attribute-name" data-bind="value: relationDefinition.mirrorRelation"/>
 | 
					                      <input type="text" value="true" class="attribute-name" data-bind="value: relationDefinition.inverseRelation"/>
 | 
				
			||||||
                    </label>
 | 
					                    </label>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </td>
 | 
					                </td>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,10 +10,6 @@
 | 
				
			|||||||
                    </button>
 | 
					                    </button>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="modal-body">
 | 
					                <div class="modal-body">
 | 
				
			||||||
                    <div class="form-group">
 | 
					 | 
				
			||||||
                        <label for="prompt-dialog-answer" id="prompt-dialog-question"></label>
 | 
					 | 
				
			||||||
                        <input type="text" class="form-control" id="prompt-dialog-answer" placeholder="">
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="modal-footer">
 | 
					                <div class="modal-footer">
 | 
				
			||||||
                    <button class="btn btn-primary btn-sm" id="prompt-dialog-ok-button">OK <kbd>enter</kbd></button>
 | 
					                    <button class="btn btn-primary btn-sm" id="prompt-dialog-ok-button">OK <kbd>enter</kbd></button>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -159,7 +159,7 @@
 | 
				
			|||||||
                  <a class="dropdown-item show-attributes-button"><kbd>Alt+A</kbd> Attributes</a>
 | 
					                  <a class="dropdown-item show-attributes-button"><kbd>Alt+A</kbd> Attributes</a>
 | 
				
			||||||
                  <a class="dropdown-item" id="show-source-button" data-bind="css: { disabled: type() != 'text' }">HTML source</a>
 | 
					                  <a class="dropdown-item" id="show-source-button" data-bind="css: { disabled: type() != 'text' }">HTML source</a>
 | 
				
			||||||
                  <a class="dropdown-item" id="upload-file-button">Upload file</a>
 | 
					                  <a class="dropdown-item" id="upload-file-button">Upload file</a>
 | 
				
			||||||
                  <a class="dropdown-item" id="export-note-to-markdown-button" data-bind="css: { disabled: type() != 'text' }">Export as markdown</a>
 | 
					                  <a class="dropdown-item" id="export-note-to-markdown-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' }">Export as markdown</a>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user