mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 21:11:30 +08:00 
			
		
		
		
	global link map WIP
This commit is contained in:
		
							parent
							
								
									43e829ca99
								
							
						
					
					
						commit
						a0caa21458
					
				
							
								
								
									
										2
									
								
								libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -169,7 +169,7 @@ export default class Entrypoints extends Component { | |||||||
|     async switchToDesktopVersionCommand() { |     async switchToDesktopVersionCommand() { | ||||||
|         utils.setCookie('trilium-device', 'desktop'); |         utils.setCookie('trilium-device', 'desktop'); | ||||||
| 
 | 
 | ||||||
|         utils.reloadFrontendApp(); |         utils.reloadFrontendApp("Switching to desktop version"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async openInWindowCommand({notePath, hoistedNoteId}) { |     async openInWindowCommand({notePath, hoistedNoteId}) { | ||||||
|  | |||||||
| @ -88,7 +88,7 @@ function processNoteChange(loadResults, ec) { | |||||||
|     loadResults.addNote(ec.entityId, ec.sourceId); |     loadResults.addNote(ec.entityId, ec.sourceId); | ||||||
| 
 | 
 | ||||||
|     if (ec.isErased && ec.entityId in froca.notes) { |     if (ec.isErased && ec.entityId in froca.notes) { | ||||||
|         utils.reloadFrontendApp(); |         utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -102,7 +102,7 @@ function processNoteChange(loadResults, ec) { | |||||||
| 
 | 
 | ||||||
| function processBranchChange(loadResults, ec) { | function processBranchChange(loadResults, ec) { | ||||||
|     if (ec.isErased && ec.entityId in froca.branches) { |     if (ec.isErased && ec.entityId in froca.branches) { | ||||||
|         utils.reloadFrontendApp(); |         utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -180,7 +180,7 @@ function processAttributeChange(loadResults, ec) { | |||||||
|     let attribute = froca.attributes[ec.entityId]; |     let attribute = froca.attributes[ec.entityId]; | ||||||
| 
 | 
 | ||||||
|     if (ec.isErased && ec.entityId in froca.attributes) { |     if (ec.isErased && ec.entityId in froca.attributes) { | ||||||
|         utils.reloadFrontendApp(); |         utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,7 +3,6 @@ import appContext from "./app_context.js"; | |||||||
| import server from "./server.js"; | import server from "./server.js"; | ||||||
| import libraryLoader from "./library_loader.js"; | import libraryLoader from "./library_loader.js"; | ||||||
| import ws from "./ws.js"; | import ws from "./ws.js"; | ||||||
| import protectedSessionHolder from "./protected_session_holder.js"; |  | ||||||
| import froca from "./froca.js"; | import froca from "./froca.js"; | ||||||
| 
 | 
 | ||||||
| function setupGlobs() { | function setupGlobs() { | ||||||
|  | |||||||
| @ -69,7 +69,7 @@ ws.subscribeToMessages(async message => { | |||||||
|         toastService.showMessage("Protected session has been started."); |         toastService.showMessage("Protected session has been started."); | ||||||
|     } |     } | ||||||
|     else if (message.type === 'protectedSessionLogout') { |     else if (message.type === 'protectedSessionLogout') { | ||||||
|         utils.reloadFrontendApp(); |         utils.reloadFrontendApp(`Protected session logout`); | ||||||
|     } |     } | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,8 @@ | |||||||
| function reloadFrontendApp() { | function reloadFrontendApp(reason) { | ||||||
|  |     if (reason) { | ||||||
|  |         logInfo("Frontend app reload: " + reason); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     window.location.reload(true); |     window.location.reload(true); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -25,7 +25,19 @@ function logError(message) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function logInfo(message) { | ||||||
|  |     console.log(utils.now(), message); | ||||||
|  | 
 | ||||||
|  |     if (ws && ws.readyState === 1) { | ||||||
|  |         ws.send(JSON.stringify({ | ||||||
|  |             type: 'log-info', | ||||||
|  |             info: message | ||||||
|  |         })); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| window.logError = logError; | window.logError = logError; | ||||||
|  | window.logInfo = logInfo; | ||||||
| 
 | 
 | ||||||
| function subscribeToMessages(messageHandler) { | function subscribeToMessages(messageHandler) { | ||||||
|     messageHandlers.push(messageHandler); |     messageHandlers.push(messageHandler); | ||||||
| @ -91,7 +103,7 @@ async function handleMessage(event) { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (message.type === 'reload-frontend') { |     if (message.type === 'reload-frontend') { | ||||||
|         utils.reloadFrontendApp(); |         utils.reloadFrontendApp("received request from backend to reload frontend"); | ||||||
|     } |     } | ||||||
|     else if (message.type === 'frontend-update') { |     else if (message.type === 'frontend-update') { | ||||||
|         await executeFrontendUpdate(message.data.entityChanges); |         await executeFrontendUpdate(message.data.entityChanges); | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| import TypeWidget from "./type_widget.js"; | import TypeWidget from "./type_widget.js"; | ||||||
| import libraryLoader from "../../services/library_loader.js"; | import libraryLoader from "../../services/library_loader.js"; | ||||||
| import server from "../../services/server.js"; | import server from "../../services/server.js"; | ||||||
| import froca from "../../services/froca.js"; |  | ||||||
| 
 | 
 | ||||||
| const TPL = `<div class="note-detail-global-link-map note-detail-printable">
 | const TPL = `<div class="note-detail-global-link-map note-detail-printable">
 | ||||||
|     <style> |     <style> | ||||||
| @ -56,7 +55,9 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | |||||||
|             .width(this.$container.width()) |             .width(this.$container.width()) | ||||||
|             .height(this.$container.height()) |             .height(this.$container.height()) | ||||||
|             .onZoom(zoom => this.setZoomLevel(zoom.k)) |             .onZoom(zoom => this.setZoomLevel(zoom.k)) | ||||||
|             .nodeRelSize(7) |             .d3AlphaDecay(0.01) | ||||||
|  |             .d3VelocityDecay(0.08) | ||||||
|  |             .nodeRelSize(node => this.noteIdToSizeMap[node.id]) | ||||||
|             .nodeCanvasObject((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx)) |             .nodeCanvasObject((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx)) | ||||||
|             .nodePointerAreaPaint((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx)) |             .nodePointerAreaPaint((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx)) | ||||||
|             .nodeLabel(node => node.name) |             .nodeLabel(node => node.name) | ||||||
| @ -70,19 +71,23 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | |||||||
|             .linkLabel(l => `${l.source.name} - <strong>${l.name}</strong> - ${l.target.name}`) |             .linkLabel(l => `${l.source.name} - <strong>${l.name}</strong> - ${l.target.name}`) | ||||||
|             .linkCanvasObject((link, ctx) => this.paintLink(link, ctx)) |             .linkCanvasObject((link, ctx) => this.paintLink(link, ctx)) | ||||||
|             .linkCanvasObjectMode(() => "after") |             .linkCanvasObjectMode(() => "after") | ||||||
|             .linkDirectionalArrowLength(4) |             .warmupTicks(10) | ||||||
|  | //            .linkDirectionalArrowLength(5)
 | ||||||
|             .linkDirectionalArrowRelPos(1) |             .linkDirectionalArrowRelPos(1) | ||||||
|             .linkWidth(2) |             .linkWidth(1) | ||||||
|             .linkColor(() => this.css.mutedTextColor) |             .linkColor(() => this.css.mutedTextColor) | ||||||
|             .d3VelocityDecay(0.2) | //            .d3VelocityDecay(0.2)
 | ||||||
|  | //            .dagMode("radialout")
 | ||||||
|             .onNodeClick(node => this.nodeClicked(node)); |             .onNodeClick(node => this.nodeClicked(node)); | ||||||
| 
 | 
 | ||||||
|         this.graph.d3Force('link').distance(50); |         this.graph.d3Force('link').distance(5); | ||||||
| 
 |         //
 | ||||||
|         this.graph.d3Force('center').strength(0.9); |         this.graph.d3Force('center').strength(0.01); | ||||||
| 
 |         //
 | ||||||
|         this.graph.d3Force('charge').strength(-30); |         this.graph.d3Force('charge').strength(-30); | ||||||
|         this.graph.d3Force('charge').distanceMax(400); | 
 | ||||||
|  | 
 | ||||||
|  |         this.graph.d3Force('charge').distanceMax(1000); | ||||||
| 
 | 
 | ||||||
|         this.renderData(await this.loadNotesAndRelations()); |         this.renderData(await this.loadNotesAndRelations()); | ||||||
|     } |     } | ||||||
| @ -113,13 +118,18 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | |||||||
| 
 | 
 | ||||||
|     paintNode(node, color, ctx) { |     paintNode(node, color, ctx) { | ||||||
|         const {x, y} = node; |         const {x, y} = node; | ||||||
|  |         const size = this.noteIdToSizeMap[node.id]; | ||||||
| 
 | 
 | ||||||
|         ctx.fillStyle = node.id === this.noteId ? 'red' : color; |         ctx.fillStyle = node.id === this.noteId ? 'red' : color; | ||||||
|         ctx.beginPath(); |         ctx.beginPath(); | ||||||
|         ctx.arc(x, y, node.id === this.noteId ? 8 : 4, 0, 2 * Math.PI, false); |         ctx.arc(x, y, size, 0, 2 * Math.PI, false); | ||||||
|         ctx.fill(); |         ctx.fill(); | ||||||
| 
 | 
 | ||||||
|         if (this.zoomLevel < 2) { |         const toRender = this.zoomLevel > 2 | ||||||
|  |             || (this.zoomLevel > 1 && size > 6) | ||||||
|  |             || (this.zoomLevel > 0.3 && size > 10); | ||||||
|  | 
 | ||||||
|  |         if (!toRender) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -132,7 +142,7 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         ctx.fillStyle = this.css.textColor; |         ctx.fillStyle = this.css.textColor; | ||||||
|         ctx.font = 5 + 'px ' + this.css.fontFamily; |         ctx.font = size + 'px ' + this.css.fontFamily; | ||||||
|         ctx.textAlign = 'center'; |         ctx.textAlign = 'center'; | ||||||
|         ctx.textBaseline = 'middle'; |         ctx.textBaseline = 'middle'; | ||||||
| 
 | 
 | ||||||
| @ -142,7 +152,7 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | |||||||
|             title = title.substr(0, 15) + "..."; |             title = title.substr(0, 15) + "..."; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         ctx.fillText(title, x, y + (node.id === this.noteId ? 11 : 7)); |         ctx.fillText(title, x, y + Math.round(size * 1.5)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     paintLink(link, ctx) { |     paintLink(link, ctx) { | ||||||
| @ -183,20 +193,16 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | |||||||
|         this.linkIdToLinkMap = {}; |         this.linkIdToLinkMap = {}; | ||||||
|         this.noteIdToLinkCountMap = {}; |         this.noteIdToLinkCountMap = {}; | ||||||
| 
 | 
 | ||||||
|         const resp = await server.post(`notes/root/link-map`, { |         const resp = await server.post(`global-link-map`); | ||||||
|             maxNotes: 1000, |  | ||||||
|             maxDepth |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         this.noteIdToLinkCountMap = {...this.noteIdToLinkCountMap, ...resp.noteIdToLinkCountMap}; |         this.noteIdToLinkCountMap = resp.noteIdToLinkCountMap; | ||||||
|  | 
 | ||||||
|  |         this.calculateSizes(resp.noteIdToDescendantCountMap); | ||||||
| 
 | 
 | ||||||
|         for (const link of resp.links) { |         for (const link of resp.links) { | ||||||
|             this.linkIdToLinkMap[link.id] = link; |             this.linkIdToLinkMap[link.id] = link; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // preload all notes
 |  | ||||||
|         const notes = await froca.getNotes(Object.keys(this.noteIdToLinkCountMap), true); |  | ||||||
| 
 |  | ||||||
|         const noteIdToLinkIdMap = {}; |         const noteIdToLinkIdMap = {}; | ||||||
|         noteIdToLinkIdMap[this.noteId] = new Set(); // for case there are no relations
 |         noteIdToLinkIdMap[this.noteId] = new Set(); // for case there are no relations
 | ||||||
|         const linksGroupedBySourceTarget = {}; |         const linksGroupedBySourceTarget = {}; | ||||||
| @ -226,11 +232,11 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return { |         return { | ||||||
|             nodes: notes.map(note => ({ |             nodes: resp.notes.map(([noteId, title, type]) => ({ | ||||||
|                 id: note.noteId, |                 id: noteId, | ||||||
|                 name: note.title, |                 name: title, | ||||||
|                 type: note.type, |                 type: type, | ||||||
|                 expanded: this.noteIdToLinkCountMap[note.noteId] === noteIdToLinkIdMap[note.noteId].size |                 expanded: true | ||||||
|             })), |             })), | ||||||
|             links: Object.values(linksGroupedBySourceTarget).map(link => ({ |             links: Object.values(linksGroupedBySourceTarget).map(link => ({ | ||||||
|                 id: link.id, |                 id: link.id, | ||||||
| @ -241,6 +247,20 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | |||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     calculateSizes(noteIdToDescendantCountMap) { | ||||||
|  |         this.noteIdToSizeMap = {}; | ||||||
|  | 
 | ||||||
|  |         for (const noteId in noteIdToDescendantCountMap) { | ||||||
|  |             this.noteIdToSizeMap[noteId] = 4; | ||||||
|  | 
 | ||||||
|  |             const count = noteIdToDescendantCountMap[noteId]; | ||||||
|  | 
 | ||||||
|  |             if (count > 0) { | ||||||
|  |                 this.noteIdToSizeMap[noteId] += 1 + Math.round(Math.log(count) / Math.log(1.5)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     renderData(data, zoomToFit = true, zoomPadding = 10) { |     renderData(data, zoomToFit = true, zoomPadding = 10) { | ||||||
|         this.graph.graphData(data); |         this.graph.graphData(data); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -79,6 +79,92 @@ function getLinkMap(req) { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| module.exports = { | function buildDescendantCountMap() { | ||||||
|     getLinkMap |     const noteIdToCountMap = {}; | ||||||
|  | 
 | ||||||
|  |     function getCount(noteId) { | ||||||
|  |         if (!(noteId in noteIdToCountMap)) { | ||||||
|  |             const note = becca.getNote(noteId); | ||||||
|  | 
 | ||||||
|  |             noteIdToCountMap[noteId] = note.children.length; | ||||||
|  | 
 | ||||||
|  |             for (const child of note.children) { | ||||||
|  |                 noteIdToCountMap[noteId] += getCount(child.noteId); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return noteIdToCountMap[noteId]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getCount('root'); | ||||||
|  | 
 | ||||||
|  |     return noteIdToCountMap; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getGlobalLinkMap() { | ||||||
|  |     const relations = Object.values(becca.attributes).filter(rel => { | ||||||
|  |         if (rel.type !== 'relation' || rel.name === 'relationMapLink' || rel.name === 'template') { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         else if (rel.name === 'imageLink') { | ||||||
|  |             const parentNote = becca.getNote(rel.noteId); | ||||||
|  | 
 | ||||||
|  |             return !parentNote.getChildNotes().find(childNote => childNote.noteId === rel.value); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const noteIdToLinkCountMap = {}; | ||||||
|  | 
 | ||||||
|  |     for (const noteId in becca.notes) { | ||||||
|  |         noteIdToLinkCountMap[noteId] = getRelations(noteId).length; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let links = Array.from(relations).map(rel => ({ | ||||||
|  |         id: rel.noteId + "-" + rel.name + "-" + rel.value, | ||||||
|  |         sourceNoteId: rel.noteId, | ||||||
|  |         targetNoteId: rel.value, | ||||||
|  |         name: rel.name | ||||||
|  |     })); | ||||||
|  | 
 | ||||||
|  |     links = []; | ||||||
|  | 
 | ||||||
|  |     const noteIds = new Set(); | ||||||
|  | 
 | ||||||
|  |     const notes = Object.values(becca.notes) | ||||||
|  |         .filter(note => !note.isArchived) | ||||||
|  |         .map(note => [ | ||||||
|  |             note.noteId, | ||||||
|  |             note.isContentAvailable() ? note.title : '[protected]', | ||||||
|  |             note.type | ||||||
|  |         ]); | ||||||
|  | 
 | ||||||
|  |     notes.forEach(([noteId]) => noteIds.add(noteId)); | ||||||
|  | 
 | ||||||
|  |     for (const branch of Object.values(becca.branches)) { | ||||||
|  |         if (!noteIds.has(branch.parentNoteId) || !noteIds.has(branch.noteId)) { | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         links.push({ | ||||||
|  |             id: branch.branchId, | ||||||
|  |             sourceNoteId: branch.parentNoteId, | ||||||
|  |             targetNoteId: branch.noteId, | ||||||
|  |             name: 'branch' | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |         notes: notes, | ||||||
|  |         noteIdToLinkCountMap, | ||||||
|  |         noteIdToDescendantCountMap: buildDescendantCountMap(), | ||||||
|  |         links: links | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  |     getLinkMap, | ||||||
|  |     getGlobalLinkMap | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -221,6 +221,7 @@ function register(app) { | |||||||
|     apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); |     apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); | ||||||
| 
 | 
 | ||||||
|     apiRoute(POST, '/api/notes/:noteId/link-map', linkMapRoute.getLinkMap); |     apiRoute(POST, '/api/notes/:noteId/link-map', linkMapRoute.getLinkMap); | ||||||
|  |     apiRoute(POST, '/api/global-link-map', linkMapRoute.getGlobalLinkMap); | ||||||
| 
 | 
 | ||||||
|     apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote); |     apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote); | ||||||
|     apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote); |     apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote); | ||||||
|  | |||||||
| @ -41,6 +41,9 @@ function init(httpServer, sessionParser) { | |||||||
|             if (message.type === 'log-error') { |             if (message.type === 'log-error') { | ||||||
|                 log.info('JS Error: ' + message.error + '\r\nStack: ' + message.stack); |                 log.info('JS Error: ' + message.error + '\r\nStack: ' + message.stack); | ||||||
|             } |             } | ||||||
|  |             else if (message.type === 'log-info') { | ||||||
|  |                 log.info('JS Info: ' + message.info); | ||||||
|  |             } | ||||||
|             else if (message.type === 'ping') { |             else if (message.type === 'ping') { | ||||||
|                 await syncMutexService.doExclusively(() => sendPing(ws)); |                 await syncMutexService.doExclusively(() => sendPing(ws)); | ||||||
|             } |             } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 zadam
						zadam