mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 04:51:31 +08:00 
			
		
		
		
	fixes, allowing conversion of note into an attachment
This commit is contained in:
		
							parent
							
								
									330e7ac08e
								
							
						
					
					
						commit
						735ac55bb8
					
				
							
								
								
									
										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
											
										
									
								
							
							
								
								
									
										28
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										28
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -15,7 +15,7 @@ | ||||
|         "@excalidraw/excalidraw": "0.15.2", | ||||
|         "archiver": "5.3.1", | ||||
|         "async-mutex": "0.4.0", | ||||
|         "axios": "1.3.6", | ||||
|         "axios": "1.4.0", | ||||
|         "better-sqlite3": "7.4.5", | ||||
|         "chokidar": "3.5.3", | ||||
|         "cls-hooked": "4.2.2", | ||||
| @ -99,7 +99,7 @@ | ||||
|         "nodemon": "2.0.22", | ||||
|         "prettier": "2.8.8", | ||||
|         "rcedit": "3.0.1", | ||||
|         "webpack": "5.80.0", | ||||
|         "webpack": "5.81.0", | ||||
|         "webpack-cli": "5.0.2" | ||||
|       }, | ||||
|       "optionalDependencies": { | ||||
| @ -2299,9 +2299,9 @@ | ||||
|       "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" | ||||
|     }, | ||||
|     "node_modules/axios": { | ||||
|       "version": "1.3.6", | ||||
|       "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", | ||||
|       "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", | ||||
|       "version": "1.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", | ||||
|       "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", | ||||
|       "dependencies": { | ||||
|         "follow-redirects": "^1.15.0", | ||||
|         "form-data": "^4.0.0", | ||||
| @ -12666,9 +12666,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/webpack": { | ||||
|       "version": "5.80.0", | ||||
|       "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.80.0.tgz", | ||||
|       "integrity": "sha512-OIMiq37XK1rWO8mH9ssfFKZsXg4n6klTEDL7S8/HqbAOBBaiy8ABvXvz0dDCXeEF9gqwxSvVk611zFPjS8hJxA==", | ||||
|       "version": "5.81.0", | ||||
|       "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.81.0.tgz", | ||||
|       "integrity": "sha512-AAjaJ9S4hYCVODKLQTgG5p5e11hiMawBwV2v8MYLE0C/6UAGLuAF4n1qa9GOwdxnicaP+5k6M5HrLmD4+gIB8Q==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "@types/eslint-scope": "^3.7.3", | ||||
| @ -14890,9 +14890,9 @@ | ||||
|       "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" | ||||
|     }, | ||||
|     "axios": { | ||||
|       "version": "1.3.6", | ||||
|       "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", | ||||
|       "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", | ||||
|       "version": "1.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", | ||||
|       "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", | ||||
|       "requires": { | ||||
|         "follow-redirects": "^1.15.0", | ||||
|         "form-data": "^4.0.0", | ||||
| @ -22757,9 +22757,9 @@ | ||||
|       "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" | ||||
|     }, | ||||
|     "webpack": { | ||||
|       "version": "5.80.0", | ||||
|       "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.80.0.tgz", | ||||
|       "integrity": "sha512-OIMiq37XK1rWO8mH9ssfFKZsXg4n6klTEDL7S8/HqbAOBBaiy8ABvXvz0dDCXeEF9gqwxSvVk611zFPjS8hJxA==", | ||||
|       "version": "5.81.0", | ||||
|       "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.81.0.tgz", | ||||
|       "integrity": "sha512-AAjaJ9S4hYCVODKLQTgG5p5e11hiMawBwV2v8MYLE0C/6UAGLuAF4n1qa9GOwdxnicaP+5k6M5HrLmD4+gIB8Q==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@types/eslint-scope": "^3.7.3", | ||||
|  | ||||
| @ -36,7 +36,7 @@ | ||||
|     "@excalidraw/excalidraw": "0.15.2", | ||||
|     "archiver": "5.3.1", | ||||
|     "async-mutex": "0.4.0", | ||||
|     "axios": "1.3.6", | ||||
|     "axios": "1.4.0", | ||||
|     "better-sqlite3": "7.4.5", | ||||
|     "chokidar": "3.5.3", | ||||
|     "cls-hooked": "4.2.2", | ||||
| @ -117,7 +117,7 @@ | ||||
|     "prettier": "2.8.8", | ||||
|     "nodemon": "2.0.22", | ||||
|     "rcedit": "3.0.1", | ||||
|     "webpack": "5.80.0", | ||||
|     "webpack": "5.81.0", | ||||
|     "webpack-cli": "5.0.2" | ||||
|   }, | ||||
|   "optionalDependencies": { | ||||
|  | ||||
| @ -2,9 +2,9 @@ | ||||
| 
 | ||||
| const utils = require('../../services/utils'); | ||||
| const dateUtils = require('../../services/date_utils'); | ||||
| const becca = require('../becca'); | ||||
| const AbstractBeccaEntity = require("./abstract_becca_entity"); | ||||
| const sql = require("../../services/sql"); | ||||
| const protectedSessionService = require("../../services/protected_session.js"); | ||||
| 
 | ||||
| const attachmentRoleToNoteTypeMapping = { | ||||
|     'image': 'image' | ||||
| @ -72,7 +72,7 @@ class BAttachment extends AbstractBeccaEntity { | ||||
|     } | ||||
| 
 | ||||
|     getNote() { | ||||
|         return becca.notes[this.parentId]; | ||||
|         return this.becca.notes[this.parentId]; | ||||
|     } | ||||
| 
 | ||||
|     /** @returns {boolean} true if the note has string content (not binary) */ | ||||
| @ -80,6 +80,12 @@ class BAttachment extends AbstractBeccaEntity { | ||||
|         return utils.isStringNote(this.type, this.mime); | ||||
|     } | ||||
| 
 | ||||
|     isContentAvailable() { | ||||
|         return !this.attachmentId // new attachment which was not encrypted yet
 | ||||
|             || !this.isProtected | ||||
|             || protectedSessionService.isProtectedSessionAvailable() | ||||
|     } | ||||
| 
 | ||||
|     /** @returns {*} */ | ||||
|     getContent() { | ||||
|         return this._getContent(); | ||||
| @ -129,15 +135,17 @@ class BAttachment extends AbstractBeccaEntity { | ||||
| 
 | ||||
|         this.markAsDeleted(); | ||||
| 
 | ||||
|         if (this.role === 'image' && this.type === 'text') { | ||||
|             const origContent = this.getContent(); | ||||
|             const oldAttachmentUrl = `api/attachment/${this.attachmentId}/image/`; | ||||
|         const parentNote = this.getNote(); | ||||
| 
 | ||||
|         if (this.role === 'image' && parentNote.type === 'text') { | ||||
|             const origContent = parentNote.getContent(); | ||||
|             const oldAttachmentUrl = `api/attachments/${this.attachmentId}/image/`; | ||||
|             const newNoteUrl = `api/images/${note.noteId}/`; | ||||
| 
 | ||||
|             const fixedContent = utils.replaceAll(origContent, oldAttachmentUrl, newNoteUrl); | ||||
| 
 | ||||
|             if (origContent !== fixedContent) { | ||||
|                 this.setContent(fixedContent); | ||||
|             if (fixedContent !== origContent) { | ||||
|                 parentNote.setContent(fixedContent); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -1436,6 +1436,28 @@ class BNote extends AbstractBeccaEntity { | ||||
| 
 | ||||
|         return cloningService.cloneNoteToBranch(this.noteId, branch.branchId); | ||||
|     } | ||||
| 
 | ||||
|     isEligibleForConversionToAttachment() { | ||||
|         if (this.type !== 'image' || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const targetRelations = this.getTargetRelations().filter(relation => relation.name === 'imageLink'); | ||||
| 
 | ||||
|         if (targetRelations.length !== 1) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const parentNote = this.getParentNotes()[0]; // at this point note can have only one parent
 | ||||
|         const referencingNote = targetRelations[0].getNote(); | ||||
| 
 | ||||
|         if (parentNote !== referencingNote || parentNote.type !== 'text' || !parentNote.isContentAvailable()) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Some notes are eligible for conversion into an attachment of its parent, note must have these properties: | ||||
|      * - it has exactly one target relation | ||||
| @ -1456,25 +1478,13 @@ class BNote extends AbstractBeccaEntity { | ||||
|      * @returns {BAttachment|null} - null if note is not eligible for conversion | ||||
|      */ | ||||
|     convertToParentAttachment(opts = {force: false}) { | ||||
|         if (this.type !== 'image' || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         const targetRelations = this.getTargetRelations().filter(relation => relation.name === 'imageLink'); | ||||
| 
 | ||||
|         if (targetRelations.length !== 1) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         const parentNote = this.getParentNotes()[0]; // at this point note can have only one parent
 | ||||
|         const referencingNote = targetRelations[0].note; | ||||
| 
 | ||||
|         if (parentNote !== referencingNote || parentNote.type !== 'text' || !parentNote.isContentAvailable()) { | ||||
|         if (!this.isEligibleForConversionToAttachment()) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         const content = this.getContent(); | ||||
| 
 | ||||
|         const parentNote = this.getParentNotes()[0]; | ||||
|         const attachment = parentNote.saveAttachment({ | ||||
|             role: 'image', | ||||
|             mime: this.mime, | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| import server from '../services/server.js'; | ||||
| import noteAttributeCache from "../services/note_attribute_cache.js"; | ||||
| import ws from "../services/ws.js"; | ||||
| import options from "../services/options.js"; | ||||
| import froca from "../services/froca.js"; | ||||
| import protectedSessionHolder from "../services/protected_session_holder.js"; | ||||
| import cssClassManager from "../services/css_class_manager.js"; | ||||
| @ -246,6 +245,27 @@ class FNote { | ||||
|         return attachments.find(att => att.attachmentId === attachmentId); | ||||
|     } | ||||
| 
 | ||||
|     isEligibleForConversionToAttachment() { | ||||
|         if (this.type !== 'image' || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const targetRelations = this.getTargetRelations().filter(relation => relation.name === 'imageLink'); | ||||
| 
 | ||||
|         if (targetRelations.length !== 1) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const parentNote = this.getParentNotes()[0]; // at this point note can have only one parent
 | ||||
|         const referencingNote = targetRelations[0].getNote(); | ||||
| 
 | ||||
|         if (parentNote !== referencingNote || parentNote.type !== 'text' || !parentNote.isContentAvailable()) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param {string} [type] - (optional) attribute type to filter | ||||
|      * @param {string} [name] - (optional) attribute name to filter | ||||
|  | ||||
| @ -30,7 +30,6 @@ const TPL = ` | ||||
|     <div class="dropdown-menu dropdown-menu-right"> | ||||
|         <a data-trigger-command="deleteAttachment" class="dropdown-item">Delete attachment</a> | ||||
|         <a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item">Convert attachment into note</a> | ||||
|         <a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item pull-attachment-into-note-button">Copy into clipboard</a> | ||||
|     </div> | ||||
| </div>`; | ||||
| 
 | ||||
| @ -47,22 +46,22 @@ export default class AttachmentActionsWidget extends BasicWidget { | ||||
|     } | ||||
| 
 | ||||
|     async deleteAttachmentCommand() { | ||||
|         if (await dialogService.confirm(`Are you sure you want to delete attachment '${this.attachment.title}'?`)) { | ||||
|             await server.remove(`attachments/${this.attachment.attachmentId}`); | ||||
| 
 | ||||
|             toastService.showMessage(`Attachment '${this.attachment.title}' has been deleted.`); | ||||
|         if (!await dialogService.confirm(`Are you sure you want to delete attachment '${this.attachment.title}'?`)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await server.remove(`attachments/${this.attachment.attachmentId}`); | ||||
|         toastService.showMessage(`Attachment '${this.attachment.title}' has been deleted.`); | ||||
|     } | ||||
| 
 | ||||
|     async convertAttachmentIntoNoteCommand() { | ||||
|         if (await dialogService.confirm(`Are you sure you want to convert attachment '${this.attachment.title}' into a separate note?`)) { | ||||
|             const {note: newNote} = await server.post(`attachments/${this.attachment.attachmentId}/convert-to-note`) | ||||
| 
 | ||||
|             toastService.showMessage(`Attachment '${this.attachment.title}' has been converted to note.`); | ||||
| 
 | ||||
|             await ws.waitForMaxKnownEntityChangeId(); | ||||
| 
 | ||||
|             await appContext.tabManager.getActiveContext().setNote(newNote.noteId); | ||||
|         if (!await dialogService.confirm(`Are you sure you want to convert attachment '${this.attachment.title}' into a separate note?`)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const {note: newNote} = await server.post(`attachments/${this.attachment.attachmentId}/convert-to-note`) | ||||
|         toastService.showMessage(`Attachment '${this.attachment.title}' has been converted to note.`); | ||||
|         await ws.waitForMaxKnownEntityChangeId(); | ||||
|         await appContext.tabManager.getActiveContext().setNote(newNote.noteId); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,11 @@ | ||||
| import NoteContextAwareWidget from "../note_context_aware_widget.js"; | ||||
| import utils from "../../services/utils.js"; | ||||
| import branchService from "../../services/branches.js"; | ||||
| import dialogService from "../../services/dialog.js"; | ||||
| import server from "../../services/server.js"; | ||||
| import toastService from "../../services/toast.js"; | ||||
| import ws from "../../services/ws.js"; | ||||
| import appContext from "../../components/app_context.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <div class="dropdown note-actions"> | ||||
| @ -25,6 +30,7 @@ const TPL = ` | ||||
|         aria-expanded="false" class="icon-action bx bx-dots-vertical-rounded"></button> | ||||
| 
 | ||||
|     <div class="dropdown-menu dropdown-menu-right"> | ||||
|         <a data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">Convert into attachment</a> | ||||
|         <a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> Re-render note</a> | ||||
|         <a data-trigger-command="findInText" class="dropdown-item find-in-text-button">Search in note <kbd data-command="findInText"></a> | ||||
|         <a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a> | ||||
| @ -45,6 +51,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
| 
 | ||||
|         this.$convertNoteIntoAttachmentButton = this.$widget.find("[data-trigger-command='convertNoteIntoAttachment']"); | ||||
|         this.$findInTextButton = this.$widget.find('.find-in-text-button'); | ||||
|         this.$printActiveNoteButton = this.$widget.find('.print-active-note-button'); | ||||
|         this.$showSourceButton = this.$widget.find('.show-source-button'); | ||||
| @ -80,6 +87,8 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { | ||||
|     } | ||||
| 
 | ||||
|     refreshWithNote(note) { | ||||
|         this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment()); | ||||
| 
 | ||||
|         this.toggleDisabled(this.$findInTextButton, ['text', 'code', 'book', 'search'].includes(note.type)); | ||||
| 
 | ||||
|         this.toggleDisabled(this.$showSourceButton, ['text', 'relationMap', 'mermaid'].includes(note.type)); | ||||
| @ -91,6 +100,28 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { | ||||
|         this.$openNoteExternallyButton.toggle(utils.isElectron()); | ||||
|     } | ||||
| 
 | ||||
|     async convertNoteIntoAttachmentCommand() { | ||||
|         if (!await dialogService.confirm(`Are you sure you want to convert note '${this.note.title}' into an attachment of the parent note?`)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const {attachment: newAttachment} = await server.post(`notes/${this.noteId}/convert-to-attachment`); | ||||
| 
 | ||||
|         if (!newAttachment) { | ||||
|             toastService.showMessage(`Converting note '${this.note.title}' failed.`); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         toastService.showMessage(`Note '${newAttachment.title}' has been converted to attachment.`); | ||||
|         await ws.waitForMaxKnownEntityChangeId(); | ||||
|         await appContext.tabManager.getActiveContext().setNote(newAttachment.parentId, { | ||||
|             viewScope: { | ||||
|                 viewMode: 'attachments', | ||||
|                 attachmentId: newAttachment.attachmentId | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     toggleDisabled($el, enable) { | ||||
|         if (enable) { | ||||
|             $el.removeAttr('disabled'); | ||||
|  | ||||
| @ -267,6 +267,19 @@ function forceSaveNoteRevision(req) { | ||||
|     note.saveNoteRevision(); | ||||
| } | ||||
| 
 | ||||
| function convertNoteToAttachment(req) { | ||||
|     const {noteId} = req.params; | ||||
|     const note = becca.getNote(noteId); | ||||
| 
 | ||||
|     if (!note) { | ||||
|         throw new NotFoundError(`Note '${noteId}' not found.`); | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         attachment: note.convertToParentAttachment({ force: true }) | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|     getNote, | ||||
|     updateNoteData, | ||||
| @ -282,5 +295,6 @@ module.exports = { | ||||
|     eraseUnusedAttachmentsNow, | ||||
|     getDeleteNotesPreview, | ||||
|     uploadModifiedFile, | ||||
|     forceSaveNoteRevision | ||||
|     forceSaveNoteRevision, | ||||
|     convertNoteToAttachment | ||||
| }; | ||||
|  | ||||
| @ -138,6 +138,7 @@ function register(app) { | ||||
|     // this "hacky" path is used for easier referencing of CSS resources
 | ||||
|     route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); | ||||
|     apiRoute(PST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveToTmpDir); | ||||
|     apiRoute(PST, '/api/notes/:noteId/convert-to-attachment', notesApiRoute.convertNoteToAttachment); | ||||
| 
 | ||||
|     apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent); | ||||
|     apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote); | ||||
|  | ||||
| @ -370,6 +370,8 @@ function checkImageAttachments(note, content) { | ||||
|         newAttachment.setContent(unknownAttachment.getContent(), { forceSave: true }); | ||||
| 
 | ||||
|         content = content.replace(`api/attachments/${unknownAttachment.attachmentId}/image`, `api/attachments/${newAttachment.attachmentId}/image`); | ||||
| 
 | ||||
|         log.info(`Copied attachment '${unknownAttachment.attachmentId}' to new '${newAttachment.attachmentId}'`); | ||||
|     } | ||||
| 
 | ||||
|     return content; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 zadam
						zadam