diff --git a/src/public/app/components/note_context.js b/src/public/app/components/note_context.js index 75cd438a7..42b1608a7 100644 --- a/src/public/app/components/note_context.js +++ b/src/public/app/components/note_context.js @@ -62,7 +62,7 @@ class NoteContext extends Component { this.notePath = resolvedNotePath; this.viewScope = opts.viewScope; - ({noteId: this.noteId, parentNoteId: this.parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(resolvedNotePath)); + ({noteId: this.noteId, parentNoteId: this.parentNoteId} = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath)); this.saveToRecentNotes(resolvedNotePath); diff --git a/src/public/app/components/root_command_executor.js b/src/public/app/components/root_command_executor.js index 98ff637f0..8f7d202d4 100644 --- a/src/public/app/components/root_command_executor.js +++ b/src/public/app/components/root_command_executor.js @@ -41,7 +41,7 @@ export default class RootCommandExecutor extends Component { } async searchInSubtreeCommand({notePath}) { - const noteId = treeService.getNoteIdFromNotePath(notePath); + const noteId = treeService.getNoteIdFromUrl(notePath); this.searchNotesCommand({ancestorNoteId: noteId}); } diff --git a/src/public/app/components/tab_manager.js b/src/public/app/components/tab_manager.js index 8f2a16942..2f461fec7 100644 --- a/src/public/app/components/tab_manager.js +++ b/src/public/app/components/tab_manager.js @@ -57,7 +57,7 @@ export default class TabManager extends Component { // preload all notes at once await froca.getNotes([ ...noteContextsToOpen.flatMap(tab => - [ treeService.getNoteIdFromNotePath(tab.notePath), tab.hoistedNoteId] + [ treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId] ), ], true); @@ -66,7 +66,7 @@ export default class TabManager extends Component { return !!openTab.active; } - const noteId = treeService.getNoteIdFromNotePath(openTab.notePath); + const noteId = treeService.getNoteIdFromUrl(openTab.notePath); if (!(noteId in froca.notes)) { // note doesn't exist so don't try to open tab for it return false; diff --git a/src/public/app/services/content_renderer.js b/src/public/app/services/content_renderer.js index 10bea265b..d21c9b070 100644 --- a/src/public/app/services/content_renderer.js +++ b/src/public/app/services/content_renderer.js @@ -94,7 +94,7 @@ async function renderText(note, options, $renderedContent) { renderMathInElement($renderedContent[0], {trust: true}); } - const getNoteIdFromLink = el => treeService.getNoteIdFromNotePath($(el).attr('href')); + const getNoteIdFromLink = el => treeService.getNoteIdFromUrl($(el).attr('href')); const referenceLinks = $renderedContent.find("a.reference-link"); const noteIdsToPrefetch = referenceLinks.map(el => getNoteIdFromLink(el)); await froca.getNotes(noteIdsToPrefetch); diff --git a/src/public/app/services/froca.js b/src/public/app/services/froca.js index f978a2fa7..de2ec7192 100644 --- a/src/public/app/services/froca.js +++ b/src/public/app/services/froca.js @@ -360,6 +360,8 @@ class Froca { opts.preview = !!opts.preview; const key = `${entityType}-${entityId}-${opts.preview}`; + console.log(key); + if (!this.blobPromises[key]) { this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob?preview=${opts.preview}`) .then(row => new FBlob(row)) diff --git a/src/public/app/services/froca_updater.js b/src/public/app/services/froca_updater.js index e3b00319a..a5dc30f8e 100644 --- a/src/public/app/services/froca_updater.js +++ b/src/public/app/services/froca_updater.js @@ -21,7 +21,13 @@ async function processEntityChanges(entityChanges) { } else if (ec.entityName === 'note_reordering') { processNoteReordering(loadResults, ec); } else if (ec.entityName === 'blobs') { - delete froca.blobPromises[ec.entityId]; + for (const affectedNoteId of ec.noteIds) { + for (const key of Object.keys(froca.blobPromises)) { + if (key.includes(affectedNoteId)) { + delete froca.blobPromises[key]; + } + } + } loadResults.addNoteContent(ec.noteIds, ec.componentId); } else if (ec.entityName === 'note_revisions') { diff --git a/src/public/app/services/hoisted_note.js b/src/public/app/services/hoisted_note.js index 5263b2eb3..f0accda7f 100644 --- a/src/public/app/services/hoisted_note.js +++ b/src/public/app/services/hoisted_note.js @@ -49,7 +49,7 @@ async function checkNoteAccess(notePath, noteContext) { const hoistedNoteId = noteContext.hoistedNoteId; if (!resolvedNotePath.includes(hoistedNoteId) && !resolvedNotePath.includes('_hidden')) { - const requestedNote = await froca.getNote(treeService.getNoteIdFromNotePath(resolvedNotePath)); + const requestedNote = await froca.getNote(treeService.getNoteIdFromUrl(resolvedNotePath)); const hoistedNote = await froca.getNote(hoistedNoteId); if (!hoistedNote.hasAncestor('_hidden') diff --git a/src/public/app/services/link.js b/src/public/app/services/link.js index f6177a05e..583f49b1c 100644 --- a/src/public/app/services/link.js +++ b/src/public/app/services/link.js @@ -43,7 +43,7 @@ async function createLink(notePath, options = {}) { const showNoteIcon = options.showNoteIcon === undefined ? false : options.showNoteIcon; const referenceLink = options.referenceLink === undefined ? false : options.referenceLink; - const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromNotePath(notePath); + const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath); const viewScope = options.viewScope || {}; const viewMode = viewScope.viewMode || 'default'; let linkTitle = options.title; @@ -174,7 +174,7 @@ function parseNavigationStateFromUrl(url) { return { notePath, - noteId: treeService.getNoteIdFromNotePath(notePath), + noteId: treeService.getNoteIdFromUrl(notePath), ntxId, hoistedNoteId, viewScope diff --git a/src/public/app/services/note_create.js b/src/public/app/services/note_create.js index 6bb0bc079..28013eb46 100644 --- a/src/public/app/services/note_create.js +++ b/src/public/app/services/note_create.js @@ -27,7 +27,7 @@ async function createNote(parentNotePath, options = {}) { [options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml()); } - const parentNoteId = treeService.getNoteIdFromNotePath(parentNotePath); + const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath); if (options.type === 'mermaid' && !options.content) { options.content = `graph TD; @@ -110,7 +110,7 @@ function parseSelectedHtml(selectedHtml) { } async function duplicateSubtree(noteId, parentNotePath) { - const parentNoteId = treeService.getNoteIdFromNotePath(parentNotePath); + const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath); const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`); await ws.waitForMaxKnownEntityChangeId(); diff --git a/src/public/app/services/tree.js b/src/public/app/services/tree.js index 47938aa88..0c8968920 100644 --- a/src/public/app/services/tree.js +++ b/src/public/app/services/tree.js @@ -103,7 +103,7 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr return effectivePathSegments; } else { - const note = await froca.getNote(getNoteIdFromNotePath(notePath)); + const note = await froca.getNote(getNoteIdFromUrl(notePath)); const bestNotePath = note.getBestNotePath(hoistedNoteId); @@ -132,26 +132,30 @@ function getParentProtectedStatus(node) { return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected; } -function getNoteIdFromNotePath(notePath) { - if (!notePath) { +function getNoteIdFromUrl(url) { + if (!url) { return null; } - const path = notePath.split("/"); + const [notePath] = url.split("?"); + const segments = notePath.split("/"); - const lastSegment = path[path.length - 1]; - - // path could have also params suffix - return lastSegment.split("?")[0]; + return segments[segments.length - 1]; } -async function getBranchIdFromNotePath(notePath) { - const {noteId, parentNoteId} = getNoteIdAndParentIdFromNotePath(notePath); +async function getBranchIdFromUrl(url) { + const {noteId, parentNoteId} = getNoteIdAndParentIdFromUrl(url); return await froca.getBranchId(parentNoteId, noteId); } -function getNoteIdAndParentIdFromNotePath(notePath) { +function getNoteIdAndParentIdFromUrl(url) { + if (!url) { + return {}; + } + + const [notePath] = url.split("?"); + if (notePath === 'root') { return { noteId: 'root', @@ -163,15 +167,12 @@ function getNoteIdAndParentIdFromNotePath(notePath) { let noteId = ''; if (notePath) { - const path = notePath.split("/"); + const segments = notePath.split("/"); - const lastSegment = path[path.length - 1]; + noteId = segments[segments.length - 1]; - // path could have also params suffix - noteId = lastSegment.split("?")[0]; - - if (path.length > 1) { - parentNoteId = path[path.length - 2]; + if (segments.length > 1) { + parentNoteId = segments[segments.length - 2]; } } @@ -288,9 +289,9 @@ export default { resolveNotePathToSegments, getParentProtectedStatus, getNotePath, - getNoteIdFromNotePath, - getNoteIdAndParentIdFromNotePath, - getBranchIdFromNotePath, + getNoteIdFromUrl, + getNoteIdAndParentIdFromUrl, + getBranchIdFromUrl, getNoteTitle, getNotePathTitle, getNoteTitleWithPathAsSuffix, diff --git a/src/public/app/widgets/dialogs/add_link.js b/src/public/app/widgets/dialogs/add_link.js index cc233c7ec..117bc052f 100644 --- a/src/public/app/widgets/dialogs/add_link.js +++ b/src/public/app/widgets/dialogs/add_link.js @@ -132,7 +132,7 @@ export default class AddLinkDialog extends BasicWidget { this.updateTitleSettingsVisibility(); - const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath); + const noteId = treeService.getNoteIdFromUrl(suggestion.notePath); if (noteId) { setDefaultLinkTitle(noteId); @@ -154,7 +154,7 @@ export default class AddLinkDialog extends BasicWidget { this.$linkTitle.val(suggestion.externalLink) } else { - const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath); + const noteId = treeService.getNoteIdFromUrl(suggestion.notePath); if (noteId) { setDefaultLinkTitle(noteId); diff --git a/src/public/app/widgets/dialogs/branch_prefix.js b/src/public/app/widgets/dialogs/branch_prefix.js index fff6d9900..2ae8133c2 100644 --- a/src/public/app/widgets/dialogs/branch_prefix.js +++ b/src/public/app/widgets/dialogs/branch_prefix.js @@ -59,7 +59,7 @@ export default class BranchPrefixDialog extends BasicWidget { } async refresh(notePath) { - const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath); + const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromUrl(notePath); if (!noteId || !parentNoteId) { return; diff --git a/src/public/app/widgets/dialogs/clone_to.js b/src/public/app/widgets/dialogs/clone_to.js index 124945d1a..68ac46bf5 100644 --- a/src/public/app/widgets/dialogs/clone_to.js +++ b/src/public/app/widgets/dialogs/clone_to.js @@ -110,7 +110,7 @@ export default class CloneToDialog extends BasicWidget { } async cloneNotesTo(notePath) { - const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath); + const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromUrl(notePath); const targetBranchId = await froca.getBranchId(parentNoteId, noteId); for (const cloneNoteId of this.clonedNoteIds) { diff --git a/src/public/app/widgets/dialogs/export.js b/src/public/app/widgets/dialogs/export.js index 02d9b999c..b63b41d0a 100644 --- a/src/public/app/widgets/dialogs/export.js +++ b/src/public/app/widgets/dialogs/export.js @@ -210,7 +210,7 @@ export default class ExportDialog extends BasicWidget { utils.openDialog(this.$widget); - const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath); + const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromUrl(notePath); this.branchId = await froca.getBranchId(parentNoteId, noteId); this.$noteTitle.text(await treeService.getNoteTitle(noteId)); diff --git a/src/public/app/widgets/dialogs/include_note.js b/src/public/app/widgets/dialogs/include_note.js index 28e8fea2a..7fb1e4c25 100644 --- a/src/public/app/widgets/dialogs/include_note.js +++ b/src/public/app/widgets/dialogs/include_note.js @@ -92,7 +92,7 @@ export default class IncludeNoteDialog extends BasicWidget { } async includeNote(notePath) { - const noteId = treeService.getNoteIdFromNotePath(notePath); + const noteId = treeService.getNoteIdFromUrl(notePath); const note = await froca.getNote(noteId); const boxSize = $("input[name='include-note-box-size']:checked").val(); diff --git a/src/public/app/widgets/dialogs/move_to.js b/src/public/app/widgets/dialogs/move_to.js index d3c74e14e..73e678834 100644 --- a/src/public/app/widgets/dialogs/move_to.js +++ b/src/public/app/widgets/dialogs/move_to.js @@ -59,7 +59,7 @@ export default class MoveToDialog extends BasicWidget { if (notePath) { this.$widget.modal('hide'); - const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath); + const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromUrl(notePath); froca.getBranchId(parentNoteId, noteId).then(branchId => this.moveNotesTo(branchId)); } else { diff --git a/src/public/app/widgets/mobile_widgets/mobile_detail_menu.js b/src/public/app/widgets/mobile_widgets/mobile_detail_menu.js index 56b237398..2bf3c90e0 100644 --- a/src/public/app/widgets/mobile_widgets/mobile_detail_menu.js +++ b/src/public/app/widgets/mobile_widgets/mobile_detail_menu.js @@ -29,7 +29,7 @@ class MobileDetailMenuWidget extends BasicWidget { } else if (command === "delete") { const notePath = appContext.tabManager.getActiveContextNotePath(); - const branchId = await treeService.getBranchIdFromNotePath(notePath); + const branchId = await treeService.getBranchIdFromUrl(notePath); if (!branchId) { throw new Error(`Cannot get branchId for notePath '${notePath}'`); diff --git a/src/public/app/widgets/ribbon_widgets/promoted_attributes.js b/src/public/app/widgets/ribbon_widgets/promoted_attributes.js index fa3edc52c..d9aa57419 100644 --- a/src/public/app/widgets/ribbon_widgets/promoted_attributes.js +++ b/src/public/app/widgets/ribbon_widgets/promoted_attributes.js @@ -297,7 +297,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { else if ($attr.attr("data-attribute-type") === "relation") { const selectedPath = $attr.getSelectedNotePath(); - value = selectedPath ? treeService.getNoteIdFromNotePath(selectedPath) : ""; + value = selectedPath ? treeService.getNoteIdFromUrl(selectedPath) : ""; } else { value = $attr.val(); diff --git a/src/public/app/widgets/type_widgets/attachment_detail.js b/src/public/app/widgets/type_widgets/attachment_detail.js index d18230721..0321d5440 100644 --- a/src/public/app/widgets/type_widgets/attachment_detail.js +++ b/src/public/app/widgets/type_widgets/attachment_detail.js @@ -45,7 +45,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget { this.$wrapper.empty(); this.children = []; - this.$linksWrapper.append( + this.$linksWrapper.empty().append( "Owning note: ", await linkService.createLink(this.noteId), ", you can also open the ", diff --git a/src/public/app/widgets/type_widgets/attachment_list.js b/src/public/app/widgets/type_widgets/attachment_list.js index 0974da227..522e40dcd 100644 --- a/src/public/app/widgets/type_widgets/attachment_list.js +++ b/src/public/app/widgets/type_widgets/attachment_list.js @@ -37,7 +37,7 @@ export default class AttachmentListTypeWidget extends TypeWidget { } async doRefresh(note) { - this.$linksWrapper.append( + this.$linksWrapper.empty().append( $('
').append( "Owning note: ", await linkService.createLink(this.noteId), diff --git a/src/services/notes.js b/src/services/notes.js index 8984286d3..8f54aaaba 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -366,17 +366,32 @@ function checkImageAttachments(note, content) { const unknownAttachments = becca.getAttachments(unknownAttachmentIds); for (const unknownAttachment of unknownAttachments) { - // the attachment belongs to a different note (was copy pasted), we need to make a copy for this note. - const newAttachment = unknownAttachment.copy(); - newAttachment.parentId = note.noteId; - newAttachment.setContent(unknownAttachment.getContent(), { forceSave: true }); + // the attachment belongs to a different note (was copy pasted). Attachments can be linked only from the note + // which owns it, so either find an existing attachment having the same content or make a copy. + let localAttachment = note.getAttachments().find(att => att.role === unknownAttachment.role && att.blobId === unknownAttachment.blobId); - content = content.replace(`api/attachments/${unknownAttachment.attachmentId}/image`, `api/attachments/${newAttachment.attachmentId}/image`); - content = content.replace(`attachmentId=${unknownAttachment.attachmentId}`, `attachmentId=${newAttachment.attachmentId}`); + if (localAttachment) { + if (localAttachment.utcDateScheduledForErasureSince) { + // the attachment is for sure linked now, so reset the scheduled deletion + localAttachment.utcDateScheduledForErasureSince = null; + localAttachment.save(); + } - ws.sendMessageToAllClients({ type: 'toast', message: `Attachment '${newAttachment.title}' has been copied to note '${note.title}'.`}); + log.info(`Found equivalent attachment '${localAttachment.attachmentId}' of note '${note.noteId}' for the linked foreign attachment '${unknownAttachment.attachmentId}' of note '${unknownAttachment.parentId}'`); + } else { + localAttachment = unknownAttachment.copy(); + localAttachment.parentId = note.noteId; + localAttachment.setContent(unknownAttachment.getContent(), {forceSave: true}); - log.info(`Copied attachment '${unknownAttachment.attachmentId}' to new '${newAttachment.attachmentId}'`); + ws.sendMessageToAllClients({ type: 'toast', message: `Attachment '${localAttachment.title}' has been copied to note '${note.title}'.`}); + log.info(`Copied attachment '${unknownAttachment.attachmentId}' of note '${unknownAttachment.parentId}' to new '${localAttachment.attachmentId}' of note '${note.noteId}'`); + } + + // replace image links + content = content.replace(`api/attachments/${unknownAttachment.attachmentId}/image`, `api/attachments/${localAttachment.attachmentId}/image`); + // replace reference links + content = content.replace(new RegExp(`href="[^"]+attachmentId=${unknownAttachment.attachmentId}[^"]*"`, "g"), + `href="#root/${localAttachment.parentId}?viewMode=attachments&attachmentId=${localAttachment.attachmentId}"`); } return { diff --git a/src/services/ws.js b/src/services/ws.js index 3bfd16ad1..cb8b132ef 100644 --- a/src/services/ws.js +++ b/src/services/ws.js @@ -150,7 +150,11 @@ function fillInAdditionalProperties(entityChange) { } else if (entityChange.entityName === 'blobs') { entityChange.noteIds = sql.getColumn("SELECT noteId FROM notes WHERE blobId = ? AND isDeleted = 0", [entityChange.entityId]); } else if (entityChange.entityName === 'attachments') { - entityChange.entity = sql.getRow(`SELECT * FROM attachments WHERE attachmentId = ?`, [entityChange.entityId]); + entityChange.entity = sql.getRow(` + SELECT attachments.*, LENGTH(blobs.content) + FROM attachments + JOIN blobs ON blobs.blobId = attachments.blobId + WHERE attachmentId = ?`, [entityChange.entityId]); } if (entityChange.entity instanceof AbstractBeccaEntity) {