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

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;