mirror of
https://github.com/zadam/trilium.git
synced 2024-12-25 08:43:03 +08:00
fixes, allowing conversion of note into an attachment
This commit is contained in:
parent
330e7ac08e
commit
735ac55bb8
12 changed files with 139 additions and 54 deletions
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…
Reference in a new issue