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",
"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",

View file

@ -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": {

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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

View file

@ -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);
}
}

View file

@ -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');

View file

@ -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
};

View file

@ -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);

View file

@ -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;