mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +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