fixes, allowing conversion of note into an attachment

This commit is contained in:
zadam 2023-05-02 22:46:39 +02:00
parent 330e7ac08e
commit 735ac55bb8
12 changed files with 139 additions and 54 deletions

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
View File

@ -15,7 +15,7 @@
"@excalidraw/excalidraw": "0.15.2", "@excalidraw/excalidraw": "0.15.2",
"archiver": "5.3.1", "archiver": "5.3.1",
"async-mutex": "0.4.0", "async-mutex": "0.4.0",
"axios": "1.3.6", "axios": "1.4.0",
"better-sqlite3": "7.4.5", "better-sqlite3": "7.4.5",
"chokidar": "3.5.3", "chokidar": "3.5.3",
"cls-hooked": "4.2.2", "cls-hooked": "4.2.2",
@ -99,7 +99,7 @@
"nodemon": "2.0.22", "nodemon": "2.0.22",
"prettier": "2.8.8", "prettier": "2.8.8",
"rcedit": "3.0.1", "rcedit": "3.0.1",
"webpack": "5.80.0", "webpack": "5.81.0",
"webpack-cli": "5.0.2" "webpack-cli": "5.0.2"
}, },
"optionalDependencies": { "optionalDependencies": {
@ -2299,9 +2299,9 @@
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.3.6", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.0", "follow-redirects": "^1.15.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@ -12666,9 +12666,9 @@
} }
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.80.0", "version": "5.81.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.80.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.81.0.tgz",
"integrity": "sha512-OIMiq37XK1rWO8mH9ssfFKZsXg4n6klTEDL7S8/HqbAOBBaiy8ABvXvz0dDCXeEF9gqwxSvVk611zFPjS8hJxA==", "integrity": "sha512-AAjaJ9S4hYCVODKLQTgG5p5e11hiMawBwV2v8MYLE0C/6UAGLuAF4n1qa9GOwdxnicaP+5k6M5HrLmD4+gIB8Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.3", "@types/eslint-scope": "^3.7.3",
@ -14890,9 +14890,9 @@
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
}, },
"axios": { "axios": {
"version": "1.3.6", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"requires": { "requires": {
"follow-redirects": "^1.15.0", "follow-redirects": "^1.15.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@ -22757,9 +22757,9 @@
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
}, },
"webpack": { "webpack": {
"version": "5.80.0", "version": "5.81.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.80.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.81.0.tgz",
"integrity": "sha512-OIMiq37XK1rWO8mH9ssfFKZsXg4n6klTEDL7S8/HqbAOBBaiy8ABvXvz0dDCXeEF9gqwxSvVk611zFPjS8hJxA==", "integrity": "sha512-AAjaJ9S4hYCVODKLQTgG5p5e11hiMawBwV2v8MYLE0C/6UAGLuAF4n1qa9GOwdxnicaP+5k6M5HrLmD4+gIB8Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/eslint-scope": "^3.7.3", "@types/eslint-scope": "^3.7.3",

View File

@ -36,7 +36,7 @@
"@excalidraw/excalidraw": "0.15.2", "@excalidraw/excalidraw": "0.15.2",
"archiver": "5.3.1", "archiver": "5.3.1",
"async-mutex": "0.4.0", "async-mutex": "0.4.0",
"axios": "1.3.6", "axios": "1.4.0",
"better-sqlite3": "7.4.5", "better-sqlite3": "7.4.5",
"chokidar": "3.5.3", "chokidar": "3.5.3",
"cls-hooked": "4.2.2", "cls-hooked": "4.2.2",
@ -117,7 +117,7 @@
"prettier": "2.8.8", "prettier": "2.8.8",
"nodemon": "2.0.22", "nodemon": "2.0.22",
"rcedit": "3.0.1", "rcedit": "3.0.1",
"webpack": "5.80.0", "webpack": "5.81.0",
"webpack-cli": "5.0.2" "webpack-cli": "5.0.2"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -2,9 +2,9 @@
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
const becca = require('../becca');
const AbstractBeccaEntity = require("./abstract_becca_entity"); const AbstractBeccaEntity = require("./abstract_becca_entity");
const sql = require("../../services/sql"); const sql = require("../../services/sql");
const protectedSessionService = require("../../services/protected_session.js");
const attachmentRoleToNoteTypeMapping = { const attachmentRoleToNoteTypeMapping = {
'image': 'image' 'image': 'image'
@ -72,7 +72,7 @@ class BAttachment extends AbstractBeccaEntity {
} }
getNote() { getNote() {
return becca.notes[this.parentId]; return this.becca.notes[this.parentId];
} }
/** @returns {boolean} true if the note has string content (not binary) */ /** @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); return utils.isStringNote(this.type, this.mime);
} }
isContentAvailable() {
return !this.attachmentId // new attachment which was not encrypted yet
|| !this.isProtected
|| protectedSessionService.isProtectedSessionAvailable()
}
/** @returns {*} */ /** @returns {*} */
getContent() { getContent() {
return this._getContent(); return this._getContent();
@ -129,15 +135,17 @@ class BAttachment extends AbstractBeccaEntity {
this.markAsDeleted(); this.markAsDeleted();
if (this.role === 'image' && this.type === 'text') { const parentNote = this.getNote();
const origContent = this.getContent();
const oldAttachmentUrl = `api/attachment/${this.attachmentId}/image/`; 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 newNoteUrl = `api/images/${note.noteId}/`;
const fixedContent = utils.replaceAll(origContent, oldAttachmentUrl, newNoteUrl); const fixedContent = utils.replaceAll(origContent, oldAttachmentUrl, newNoteUrl);
if (origContent !== fixedContent) { if (fixedContent !== origContent) {
this.setContent(fixedContent); parentNote.setContent(fixedContent);
} }
} }

View File

@ -1436,6 +1436,28 @@ class BNote extends AbstractBeccaEntity {
return cloningService.cloneNoteToBranch(this.noteId, branch.branchId); 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: * Some notes are eligible for conversion into an attachment of its parent, note must have these properties:
* - it has exactly one target relation * - 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 * @returns {BAttachment|null} - null if note is not eligible for conversion
*/ */
convertToParentAttachment(opts = {force: false}) { convertToParentAttachment(opts = {force: false}) {
if (this.type !== 'image' || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) { if (!this.isEligibleForConversionToAttachment()) {
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()) {
return null; return null;
} }
const content = this.getContent(); const content = this.getContent();
const parentNote = this.getParentNotes()[0];
const attachment = parentNote.saveAttachment({ const attachment = parentNote.saveAttachment({
role: 'image', role: 'image',
mime: this.mime, mime: this.mime,

View File

@ -1,7 +1,6 @@
import server from '../services/server.js'; import server from '../services/server.js';
import noteAttributeCache from "../services/note_attribute_cache.js"; import noteAttributeCache from "../services/note_attribute_cache.js";
import ws from "../services/ws.js"; import ws from "../services/ws.js";
import options from "../services/options.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import protectedSessionHolder from "../services/protected_session_holder.js"; import protectedSessionHolder from "../services/protected_session_holder.js";
import cssClassManager from "../services/css_class_manager.js"; import cssClassManager from "../services/css_class_manager.js";
@ -246,6 +245,27 @@ class FNote {
return attachments.find(att => att.attachmentId === attachmentId); 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} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter * @param {string} [name] - (optional) attribute name to filter

View File

@ -30,7 +30,6 @@ const TPL = `
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a data-trigger-command="deleteAttachment" class="dropdown-item">Delete attachment</a> <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">Convert attachment into note</a>
<a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item pull-attachment-into-note-button">Copy into clipboard</a>
</div> </div>
</div>`; </div>`;
@ -47,22 +46,22 @@ export default class AttachmentActionsWidget extends BasicWidget {
} }
async deleteAttachmentCommand() { async deleteAttachmentCommand() {
if (await dialogService.confirm(`Are you sure you want to delete attachment '${this.attachment.title}'?`)) { if (!await dialogService.confirm(`Are you sure you want to delete attachment '${this.attachment.title}'?`)) {
await server.remove(`attachments/${this.attachment.attachmentId}`); return;
toastService.showMessage(`Attachment '${this.attachment.title}' has been deleted.`);
} }
await server.remove(`attachments/${this.attachment.attachmentId}`);
toastService.showMessage(`Attachment '${this.attachment.title}' has been deleted.`);
} }
async convertAttachmentIntoNoteCommand() { async convertAttachmentIntoNoteCommand() {
if (await dialogService.confirm(`Are you sure you want to convert attachment '${this.attachment.title}' into a separate note?`)) { 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`) const {note: newNote} = await server.post(`attachments/${this.attachment.attachmentId}/convert-to-note`)
toastService.showMessage(`Attachment '${this.attachment.title}' has been converted to note.`); toastService.showMessage(`Attachment '${this.attachment.title}' has been converted to note.`);
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(newNote.noteId); await appContext.tabManager.getActiveContext().setNote(newNote.noteId);
} }
} }
}

View File

@ -1,6 +1,11 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js"; import NoteContextAwareWidget from "../note_context_aware_widget.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import branchService from "../../services/branches.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 = ` const TPL = `
<div class="dropdown note-actions"> <div class="dropdown note-actions">
@ -25,6 +30,7 @@ const TPL = `
aria-expanded="false" class="icon-action bx bx-dots-vertical-rounded"></button> aria-expanded="false" class="icon-action bx bx-dots-vertical-rounded"></button>
<div class="dropdown-menu dropdown-menu-right"> <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="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="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> <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() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$convertNoteIntoAttachmentButton = this.$widget.find("[data-trigger-command='convertNoteIntoAttachment']");
this.$findInTextButton = this.$widget.find('.find-in-text-button'); this.$findInTextButton = this.$widget.find('.find-in-text-button');
this.$printActiveNoteButton = this.$widget.find('.print-active-note-button'); this.$printActiveNoteButton = this.$widget.find('.print-active-note-button');
this.$showSourceButton = this.$widget.find('.show-source-button'); this.$showSourceButton = this.$widget.find('.show-source-button');
@ -80,6 +87,8 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
} }
refreshWithNote(note) { refreshWithNote(note) {
this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment());
this.toggleDisabled(this.$findInTextButton, ['text', 'code', 'book', 'search'].includes(note.type)); this.toggleDisabled(this.$findInTextButton, ['text', 'code', 'book', 'search'].includes(note.type));
this.toggleDisabled(this.$showSourceButton, ['text', 'relationMap', 'mermaid'].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()); 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) { toggleDisabled($el, enable) {
if (enable) { if (enable) {
$el.removeAttr('disabled'); $el.removeAttr('disabled');

View File

@ -267,6 +267,19 @@ function forceSaveNoteRevision(req) {
note.saveNoteRevision(); 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 = { module.exports = {
getNote, getNote,
updateNoteData, updateNoteData,
@ -282,5 +295,6 @@ module.exports = {
eraseUnusedAttachmentsNow, eraseUnusedAttachmentsNow,
getDeleteNotesPreview, getDeleteNotesPreview,
uploadModifiedFile, uploadModifiedFile,
forceSaveNoteRevision forceSaveNoteRevision,
convertNoteToAttachment
}; };

View File

@ -138,6 +138,7 @@ function register(app) {
// this "hacky" path is used for easier referencing of CSS resources // this "hacky" path is used for easier referencing of CSS resources
route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); 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/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-to/:parentBranchId', branchesApiRoute.moveBranchToParent);
apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote); apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote);

View File

@ -370,6 +370,8 @@ function checkImageAttachments(note, content) {
newAttachment.setContent(unknownAttachment.getContent(), { forceSave: true }); newAttachment.setContent(unknownAttachment.getContent(), { forceSave: true });
content = content.replace(`api/attachments/${unknownAttachment.attachmentId}/image`, `api/attachments/${newAttachment.attachmentId}/image`); 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; return content;