From 5d6d9ab6d61b5893e20da0a27eadec9511b7b96d Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 3 Apr 2023 23:47:24 +0200 Subject: [PATCH] wip attachment support --- db/migrations/0217__attachments.sql | 3 +- db/schema.sql | 3 +- src/becca/entities/battachment.js | 12 +++-- src/public/app/components/entrypoints.js | 8 +-- src/public/app/components/note_context.js | 6 +-- .../app/components/root_command_executor.js | 27 +++++++++- src/public/app/components/tab_manager.js | 15 +++--- src/public/app/entities/fattachment.js | 33 ++++++++++++ src/public/app/entities/fnote.js | 20 +++++++ src/public/app/entities/fnote_complement.js | 1 + src/public/app/menus/link_context_menu.js | 8 +-- src/public/app/services/froca.js | 5 ++ src/public/app/services/froca_updater.js | 39 +++++++++++++- src/public/app/services/link.js | 30 +++++++---- src/public/app/widgets/attachment_detail.js | 10 ++-- .../widgets/buttons/launcher/note_launcher.js | 2 +- .../buttons/open_note_button_widget.js | 2 +- .../containers/split_note_container.js | 4 +- .../app/widgets/note_context_aware_widget.js | 4 ++ src/public/app/widgets/note_detail.js | 13 +++-- src/public/app/widgets/note_map.js | 2 +- src/public/app/widgets/note_title.js | 28 ++++++++-- .../widgets/type_widgets/attachment_detail.js | 54 +++++++++++++++++++ .../{attachments.js => attachment_list.js} | 12 ++--- src/routes/index.js | 4 +- src/services/window.js | 6 +-- src/views/desktop.ejs | 1 + 27 files changed, 289 insertions(+), 63 deletions(-) create mode 100644 src/public/app/entities/fattachment.js create mode 100644 src/public/app/widgets/type_widgets/attachment_detail.js rename src/public/app/widgets/type_widgets/{attachments.js => attachment_list.js} (83%) diff --git a/db/migrations/0217__attachments.sql b/db/migrations/0217__attachments.sql index 8d87de901..121c02414 100644 --- a/db/migrations/0217__attachments.sql +++ b/db/migrations/0217__attachments.sql @@ -7,8 +7,9 @@ CREATE TABLE IF NOT EXISTS "attachments" title TEXT not null, isProtected INT not null DEFAULT 0, blobId TEXT DEFAULT null, - utcDateScheduledForDeletionSince TEXT DEFAULT NULL, + dateModified TEXT NOT NULL, utcDateModified TEXT not null, + utcDateScheduledForDeletionSince TEXT DEFAULT NULL, isDeleted INT not null, deleteId TEXT DEFAULT NULL); diff --git a/db/schema.sql b/db/schema.sql index 9f1941680..2d770999a 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -118,8 +118,9 @@ CREATE TABLE IF NOT EXISTS "attachments" title TEXT not null, isProtected INT not null DEFAULT 0, blobId TEXT DEFAULT null, - utcDateScheduledForDeletionSince TEXT DEFAULT NULL, + dateModified TEXT NOT NULL, utcDateModified TEXT not null, + utcDateScheduledForDeletionSince TEXT DEFAULT NULL, isDeleted INT not null, deleteId TEXT DEFAULT NULL); CREATE INDEX IDX_attachments_parentId_role diff --git a/src/becca/entities/battachment.js b/src/becca/entities/battachment.js index b653b2233..53a2034bd 100644 --- a/src/becca/entities/battachment.js +++ b/src/becca/entities/battachment.js @@ -6,6 +6,8 @@ const becca = require('../becca'); const AbstractBeccaEntity = require("./abstract_becca_entity"); /** + * FIXME: how to order attachments? + * * Attachment represent data related/attached to the note. Conceptually similar to attributes, but intended for * larger amounts of data and generally not accessible to the user. * @@ -45,9 +47,11 @@ class BAttachment extends AbstractBeccaEntity { /** @type {boolean} */ this.isProtected = !!row.isProtected; /** @type {string} */ - this.utcDateScheduledForDeletionSince = row.utcDateScheduledForDeletionSince; + this.dateModified = row.dateModified; /** @type {string} */ this.utcDateModified = row.utcDateModified; + /** @type {string} */ + this.utcDateScheduledForDeletionSince = row.utcDateScheduledForDeletionSince; } getNote() { @@ -76,6 +80,7 @@ class BAttachment extends AbstractBeccaEntity { beforeSaving() { super.beforeSaving(); + this.dateModified = dateUtils.localNowDateTime(); this.utcDateModified = dateUtils.utcNowDateTime(); } @@ -89,8 +94,9 @@ class BAttachment extends AbstractBeccaEntity { blobId: this.blobId, isProtected: !!this.isProtected, isDeleted: false, - utcDateScheduledForDeletionSince: this.utcDateScheduledForDeletionSince, - utcDateModified: this.utcDateModified + dateModified: this.dateModified, + utcDateModified: this.utcDateModified, + utcDateScheduledForDeletionSince: this.utcDateScheduledForDeletionSince }; } diff --git a/src/public/app/components/entrypoints.js b/src/public/app/components/entrypoints.js index 68117280e..7a4535e10 100644 --- a/src/public/app/components/entrypoints.js +++ b/src/public/app/components/entrypoints.js @@ -38,7 +38,7 @@ export default class Entrypoints extends Component { await ws.waitForMaxKnownEntityChangeId(); - await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, true); + await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, {activate: true}); appContext.triggerEvent('focusAndSelectTitle', {isNewNote: true}); } @@ -135,7 +135,7 @@ export default class Entrypoints extends Component { utils.reloadFrontendApp("Switching to mobile version"); } - async openInWindowCommand({notePath, hoistedNoteId}) { + async openInWindowCommand({notePath, hoistedNoteId, viewScope}) { if (!hoistedNoteId) { hoistedNoteId = 'root'; } @@ -143,10 +143,10 @@ export default class Entrypoints extends Component { if (utils.isElectron()) { const {ipcRenderer} = utils.dynamicRequire('electron'); - ipcRenderer.send('create-extra-window', {notePath, hoistedNoteId}); + ipcRenderer.send('create-extra-window', {notePath, hoistedNoteId, viewScope}); } else { - const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extra=1#${notePath}`; + const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1&extraHoistedNoteId=${hoistedNoteId}&extraViewScope=${JSON.stringify(viewScope)}#${notePath}`; window.open(url, '', 'width=1000,height=800'); } diff --git a/src/public/app/components/note_context.js b/src/public/app/components/note_context.js index 5bed983c7..2a6ec1cec 100644 --- a/src/public/app/components/note_context.js +++ b/src/public/app/components/note_context.js @@ -53,8 +53,8 @@ class NoteContext extends Component { this.notePath = resolvedNotePath; ({noteId: this.noteId, parentNoteId: this.parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(resolvedNotePath)); - this.resetViewScope(); - this.viewScope.viewMode = opts.viewMode || "default"; + this.viewScope = opts.viewScope || {}; + this.viewScope.viewMode = this.viewScope.viewMode || "default"; this.saveToRecentNotes(resolvedNotePath); @@ -187,7 +187,7 @@ class NoteContext extends Component { notePath: this.notePath, hoistedNoteId: this.hoistedNoteId, active: this.isActive(), - viewMode: this.viewScope.viewMode + viewScope: this.viewScope } } diff --git a/src/public/app/components/root_command_executor.js b/src/public/app/components/root_command_executor.js index 930cda706..6bf44a3f2 100644 --- a/src/public/app/components/root_command_executor.js +++ b/src/public/app/components/root_command_executor.js @@ -117,7 +117,12 @@ export default class RootCommandExecutor extends Component { const notePath = appContext.tabManager.getActiveContextNotePath(); if (notePath) { - await appContext.tabManager.openContextWithNote(notePath, { activate: true, viewMode: 'source' }); + await appContext.tabManager.openContextWithNote(notePath, { + activate: true, + viewScope: { + viewMode: 'source' + } + }); } } @@ -125,7 +130,25 @@ export default class RootCommandExecutor extends Component { const notePath = appContext.tabManager.getActiveContextNotePath(); if (notePath) { - await appContext.tabManager.openContextWithNote(notePath, { activate: true, viewMode: 'attachments' }); + await appContext.tabManager.openContextWithNote(notePath, { + activate: true, + viewScope: { + viewMode: 'attachments' + } + }); + } + } + + async showAttachmentDetailCommand() { + const notePath = appContext.tabManager.getActiveContextNotePath(); + + if (notePath) { + await appContext.tabManager.openContextWithNote(notePath, { + activate: true, + viewScope: { + viewMode: 'attachments' + } + }); } } } diff --git a/src/public/app/components/tab_manager.js b/src/public/app/components/tab_manager.js index 92e94b218..ddf8860a9 100644 --- a/src/public/app/components/tab_manager.js +++ b/src/public/app/components/tab_manager.js @@ -86,7 +86,8 @@ export default class TabManager extends Component { filteredTabs.push({ notePath: notePathInUrl || 'root', active: true, - hoistedNoteId: glob.extraHoistedNoteId || 'root' + hoistedNoteId: glob.extraHoistedNoteId || 'root', + viewScope: glob.extraViewScope || {} }); } @@ -101,7 +102,7 @@ export default class TabManager extends Component { ntxId: tab.ntxId, mainNtxId: tab.mainNtxId, hoistedNoteId: tab.hoistedNoteId, - viewMode: tab.viewMode + viewScope: tab.viewScope }); } }); @@ -271,7 +272,7 @@ export default class TabManager extends Component { /** * If the requested notePath is within current note hoisting scope then keep the note hoisting also for the new tab. */ - async openTabWithNoteWithHoisting(notePath, activate = false) { + async openTabWithNoteWithHoisting(notePath, opts = {}) { const noteContext = this.getActiveContext(); let hoistedNoteId = 'root'; @@ -283,7 +284,9 @@ export default class TabManager extends Component { } } - return this.openContextWithNote(notePath, { activate, hoistedNoteId }); + opts.hoistedNoteId = hoistedNoteId; + + return this.openContextWithNote(notePath, opts); } async openContextWithNote(notePath, opts = {}) { @@ -291,7 +294,7 @@ export default class TabManager extends Component { const ntxId = opts.ntxId || null; const mainNtxId = opts.mainNtxId || null; const hoistedNoteId = opts.hoistedNoteId || 'root'; - const viewMode = opts.viewMode || "default"; + const viewScope = opts.viewScope || { viewMode: "default" }; const noteContext = await this.openEmptyTab(ntxId, hoistedNoteId, mainNtxId); @@ -299,7 +302,7 @@ export default class TabManager extends Component { await noteContext.setNote(notePath, { // if activate is false then send normal noteSwitched event triggerSwitchEvent: !activate, - viewMode: viewMode + viewScope: viewScope }); } diff --git a/src/public/app/entities/fattachment.js b/src/public/app/entities/fattachment.js new file mode 100644 index 000000000..0846e2690 --- /dev/null +++ b/src/public/app/entities/fattachment.js @@ -0,0 +1,33 @@ +class FAttachment { + constructor(froca, row) { + this.froca = froca; + + this.update(row); + } + + update(row) { + /** @type {string} */ + this.attachmentId = row.attachmentId; + /** @type {string} */ + this.parentId = row.parentId; + /** @type {string} */ + this.role = row.role; + /** @type {string} */ + this.mime = row.mime; + /** @type {string} */ + this.title = row.title; + /** @type {string} */ + this.dateModified = row.dateModified; + /** @type {string} */ + this.utcDateModified = row.utcDateModified; + /** @type {string} */ + this.utcDateScheduledForDeletionSince = row.utcDateScheduledForDeletionSince; + + this.froca.attachments[this.attachmentId] = this; + } + + /** @returns {FNote} */ + getNote() { + return this.froca.notes[this.parentId]; + } +} diff --git a/src/public/app/entities/fnote.js b/src/public/app/entities/fnote.js index 6cf6d1b58..cb0629836 100644 --- a/src/public/app/entities/fnote.js +++ b/src/public/app/entities/fnote.js @@ -51,6 +51,9 @@ class FNote { /** @type {Object.} */ this.childToBranch = {}; + /** @type {FAttachment[]|null} */ + this.attachments = null; // lazy loaded + this.update(row); } @@ -225,6 +228,23 @@ class FNote { return await this.froca.getNotes(this.children); } + /** @returns {Promise} */ + async getAttachments() { + if (!this.attachments) { + this.attachments = (await server.get(`notes/${this.noteId}/attachments`)) + .map(row => new FAttachment(froca, row)); + } + + return this.attachments; + } + + /** @returns {Promise} */ + async getAttachmentById(attachmentId) { + const attachments = await this.getAttachments(); + + return attachments.find(att => att.attachmentId === attachmentId); + } + /** * @param {string} [type] - (optional) attribute type to filter * @param {string} [name] - (optional) attribute name to filter diff --git a/src/public/app/entities/fnote_complement.js b/src/public/app/entities/fnote_complement.js index 4884e757a..39253afcd 100644 --- a/src/public/app/entities/fnote_complement.js +++ b/src/public/app/entities/fnote_complement.js @@ -1,4 +1,5 @@ /** + * FIXME: probably make it a FBlob * Complements the FNote with the main note content and other extra attributes */ class FNoteComplement { diff --git a/src/public/app/menus/link_context_menu.js b/src/public/app/menus/link_context_menu.js index 9e4d83dc0..cd9c47043 100644 --- a/src/public/app/menus/link_context_menu.js +++ b/src/public/app/menus/link_context_menu.js @@ -1,7 +1,7 @@ import contextMenu from "./context_menu.js"; import appContext from "../components/app_context.js"; -function openContextMenu(notePath, hoistedNoteId, e) { +function openContextMenu(notePath, e, viewScope = {}, hoistedNoteId = null) { contextMenu.show({ x: e.pageX, y: e.pageY, @@ -16,16 +16,16 @@ function openContextMenu(notePath, hoistedNoteId, e) { } if (command === 'openNoteInNewTab') { - appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId }); + appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope }); } else if (command === 'openNoteInNewSplit') { const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); const {ntxId} = subContexts[subContexts.length - 1]; - appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath, hoistedNoteId}); + appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath, hoistedNoteId, viewScope}); } else if (command === 'openNoteInNewWindow') { - appContext.triggerCommand('openInWindow', {notePath, hoistedNoteId}); + appContext.triggerCommand('openInWindow', {notePath, hoistedNoteId, viewScope}); } } }); diff --git a/src/public/app/services/froca.js b/src/public/app/services/froca.js index d20a55578..1d775ec11 100644 --- a/src/public/app/services/froca.js +++ b/src/public/app/services/froca.js @@ -34,6 +34,10 @@ class Froca { /** @type {Object.} */ this.attributes = {}; + /** @type {Object.} */ + this.attachments = {}; + + // FIXME /** @type {Object.>} */ this.blobPromises = {}; @@ -311,6 +315,7 @@ class Froca { } /** + * // FIXME * @returns {Promise} */ async getNoteComplement(noteId) { diff --git a/src/public/app/services/froca_updater.js b/src/public/app/services/froca_updater.js index d02af030e..ba04a6d0c 100644 --- a/src/public/app/services/froca_updater.js +++ b/src/public/app/services/froca_updater.js @@ -34,7 +34,7 @@ async function processEntityChanges(entityChanges) { loadResults.addOption(ec.entity.name); } else if (ec.entityName === 'attachments') { - loadResults.addAttachment(ec.entity); + processAttachment(loadResults, ec); } else if (ec.entityName === 'etapi_tokens') { // NOOP } @@ -231,6 +231,43 @@ function processAttributeChange(loadResults, ec) { } } +function processAttachment(loadResults, ec) { + if (ec.isErased && ec.entityId in froca.attachments) { + utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`); + return; + } + + const attachment = froca.attachments[ec.entityId]; + + if (ec.isErased || ec.entity?.isDeleted) { + if (attachment) { + const note = attachment.getNote(); + + if (note && note.attachments) { + note.attachments = note.attachments.filter(att => att.attachmentId !== attachment.attachmentId); + } + + loadResults.addAttachment(ec.entity); + + delete froca.attachments[ec.entityId]; + } + + return; + } + + if (attachment) { + attachment.update(ec.entity); + } else { + const note = froca.notes[ec.entity.parentId]; + + if (note && note.attachments) { + note.attachments.push(new FAttachment(froca, ec.entity)); + } + } + + loadResults.addAttachment(ec.entity); +} + export default { processEntityChanges } diff --git a/src/public/app/services/link.js b/src/public/app/services/link.js index c7d75c132..4daef9eca 100644 --- a/src/public/app/services/link.js +++ b/src/public/app/services/link.js @@ -87,7 +87,16 @@ function getNotePathFromLink($link) { const url = $link.attr('href'); - return url ? getNotePathFromUrl(url) : null; + const notePath = url ? getNotePathFromUrl(url) : null; + const viewScope = { + viewMode: $link.attr('data-view-mode'), + attachmentId: $link.attr('data-attachment-id'), + }; + + return { + notePath, + viewScope + }; } function goToLink(evt) { @@ -101,22 +110,25 @@ function goToLink(evt) { evt.preventDefault(); evt.stopPropagation(); - const notePath = getNotePathFromLink($link); + const {notePath, viewScope} = getNotePathFromLink($link); const ctrlKey = utils.isCtrlKey(evt); + const isLeftClick = evt.which === 1; + const isMiddleClick = evt.which === 2; + const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick; if (notePath) { - if ((evt.which === 1 && ctrlKey) || evt.which === 2) { - appContext.tabManager.openTabWithNoteWithHoisting(notePath); + if (openInNewTab) { + appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope }); } - else if (evt.which === 1) { + else if (isLeftClick) { const ntxId = $(evt.target).closest("[data-ntx-id]").attr("data-ntx-id"); const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext(); - noteContext.setNote(notePath).then(() => { + noteContext.setNote(notePath, { viewScope }).then(() => { if (noteContext !== appContext.tabManager.getActiveContext()) { appContext.tabManager.activateNoteContext(noteContext.ntxId); } @@ -124,7 +136,7 @@ function goToLink(evt) { } } else { - if ((evt.which === 1 && ctrlKey) || evt.which === 2 + if (openInNewTab || $link.hasClass("ck-link-actions__preview") // within edit link dialog single click suffices || $link.closest("[contenteditable]").length === 0 // outside of CKEditor single click suffices ) { @@ -147,7 +159,7 @@ function goToLink(evt) { function linkContextMenu(e) { const $link = $(e.target).closest("a"); - const notePath = getNotePathFromLink($link); + const {notePath, viewScope} = getNotePathFromLink($link); if (!notePath) { return; @@ -155,7 +167,7 @@ function linkContextMenu(e) { e.preventDefault(); - linkContextMenuService.openContextMenu(notePath, null, e); + linkContextMenuService.openContextMenu(notePath, e, viewScope, null); } async function loadReferenceLinkTitle(noteId, $el) { diff --git a/src/public/app/widgets/attachment_detail.js b/src/public/app/widgets/attachment_detail.js index 0ec796415..b853cdc88 100644 --- a/src/public/app/widgets/attachment_detail.js +++ b/src/public/app/widgets/attachment_detail.js @@ -37,7 +37,7 @@ const TPL = `
-

+

@@ -73,7 +73,7 @@ export default class AttachmentDetailWidget extends BasicWidget { .html() ); this.$wrapper = this.$widget.find('.attachment-detail-wrapper'); - this.$wrapper.find('.attachment-title').text(this.attachment.title); + this.$wrapper.find('.attachment-title a').text(this.attachment.title); this.$wrapper.find('.attachment-details') .text(`Role: ${this.attachment.role}, Size: ${utils.formatSize(this.attachment.contentLength)}`); this.$wrapper.find('.attachment-actions-container').append(this.attachmentActionsWidget.render()); @@ -90,9 +90,11 @@ export default class AttachmentDetailWidget extends BasicWidget { } } - async entitiesReloadedEvent({loadResults}) { - console.log("AttachmentDetailWidget: entitiesReloadedEvent"); + openAttachmentDetailCommand() { + } + + async entitiesReloadedEvent({loadResults}) { const attachmentChange = loadResults.getAttachments().find(att => att.attachmentId === this.attachment.attachmentId); if (attachmentChange) { diff --git a/src/public/app/widgets/buttons/launcher/note_launcher.js b/src/public/app/widgets/buttons/launcher/note_launcher.js index 956d03f38..001f94a03 100644 --- a/src/public/app/widgets/buttons/launcher/note_launcher.js +++ b/src/public/app/widgets/buttons/launcher/note_launcher.js @@ -27,7 +27,7 @@ export default class NoteLauncher extends AbstractLauncher { const hoistedNoteId = this.getHoistedNoteId(); - linkContextMenuService.openContextMenu(targetNoteId, hoistedNoteId, evt); + linkContextMenuService.openContextMenu(targetNoteId, evt, {}, hoistedNoteId); }); } diff --git a/src/public/app/widgets/buttons/open_note_button_widget.js b/src/public/app/widgets/buttons/open_note_button_widget.js index 511c5353f..79f5b1b9f 100644 --- a/src/public/app/widgets/buttons/open_note_button_widget.js +++ b/src/public/app/widgets/buttons/open_note_button_widget.js @@ -13,7 +13,7 @@ export default class OpenNoteButtonWidget extends OnClickButtonWidget { .icon(() => this.noteToOpen.getIcon()) .onClick((widget, evt) => this.launch(evt)) .onAuxClick((widget, evt) => this.launch(evt)) - .onContextMenu(evt => linkContextMenuService.openContextMenu(this.noteToOpen.noteId, null, evt)); + .onContextMenu(evt => linkContextMenuService.openContextMenu(this.noteToOpen.noteId, evt)); } async launch(evt) { diff --git a/src/public/app/widgets/containers/split_note_container.js b/src/public/app/widgets/containers/split_note_container.js index 66356164d..e706c6447 100644 --- a/src/public/app/widgets/containers/split_note_container.js +++ b/src/public/app/widgets/containers/split_note_container.js @@ -34,7 +34,7 @@ export default class SplitNoteContainer extends FlexContainer { this.child(widget); } - async openNewNoteSplitEvent({ntxId, notePath, hoistedNoteId}) { + async openNewNoteSplitEvent({ntxId, notePath, hoistedNoteId, viewScope}) { const mainNtxId = appContext.tabManager.getActiveMainContext().ntxId; if (!ntxId) { @@ -63,7 +63,7 @@ export default class SplitNoteContainer extends FlexContainer { await appContext.tabManager.activateNoteContext(noteContext.ntxId); if (notePath) { - await noteContext.setNote(notePath); + await noteContext.setNote(notePath, viewScope); } else { await noteContext.setEmpty(); diff --git a/src/public/app/widgets/note_context_aware_widget.js b/src/public/app/widgets/note_context_aware_widget.js index 0d96abff7..bc1b72180 100644 --- a/src/public/app/widgets/note_context_aware_widget.js +++ b/src/public/app/widgets/note_context_aware_widget.js @@ -61,6 +61,10 @@ export default class NoteContextAwareWidget extends BasicWidget { } } + /** + * @param {FNote} note + * @returns {Promise} + */ async refreshWithNote(note) {} async noteSwitchedEvent({noteContext, notePath}) { diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js index bef368ba2..6b6302c85 100644 --- a/src/public/app/widgets/note_detail.js +++ b/src/public/app/widgets/note_detail.js @@ -27,7 +27,8 @@ import NoteMapTypeWidget from "./type_widgets/note_map.js"; import WebViewTypeWidget from "./type_widgets/web_view.js"; import DocTypeWidget from "./type_widgets/doc.js"; import ContentWidgetTypeWidget from "./type_widgets/content_widget.js"; -import AttachmentsTypeWidget from "./type_widgets/attachments.js"; +import AttachmentListTypeWidget from "./type_widgets/attachment_list.js"; +import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js"; const TPL = `
@@ -63,7 +64,8 @@ const typeWidgetClasses = { 'webView': WebViewTypeWidget, 'doc': DocTypeWidget, 'contentWidget': ContentWidgetTypeWidget, - 'attachments': AttachmentsTypeWidget + 'attachmentDetail': AttachmentDetailTypeWidget, + 'attachmentList': AttachmentListTypeWidget }; export default class NoteDetailWidget extends NoteContextAwareWidget { @@ -188,11 +190,12 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { } let type = note.type; + const viewScope = this.noteContext.viewScope; - if (type === 'text' && this.noteContext.viewScope.viewMode === 'source') { + if (type === 'text' && viewScope.viewMode === 'source') { type = 'readOnlyCode'; - } else if (this.noteContext.viewScope.viewMode === 'attachments') { - type = 'attachments'; + } else if (viewScope.viewMode === 'attachments') { + type = viewScope.attachmentId ? 'attachmentDetail' : 'attachmentList'; } else if (type === 'text' && await this.noteContext.isReadOnly()) { type = 'readOnlyText'; } else if ((type === 'code' || type === 'mermaid') && await this.noteContext.isReadOnly()) { diff --git a/src/public/app/widgets/note_map.js b/src/public/app/widgets/note_map.js index ccd659683..99b779c5d 100644 --- a/src/public/app/widgets/note_map.js +++ b/src/public/app/widgets/note_map.js @@ -113,7 +113,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { .linkWidth(1) .linkColor(() => this.css.mutedTextColor) .onNodeClick(node => appContext.tabManager.getActiveContext().setNote(node.id)) - .onNodeRightClick((node, e) => linkContextMenuService.openContextMenu(node.id, null, e)); + .onNodeRightClick((node, e) => linkContextMenuService.openContextMenu(node.id, e)); if (this.mapType === 'link') { this.graph diff --git a/src/public/app/widgets/note_title.js b/src/public/app/widgets/note_title.js index 83a5e8a15..24290b2b1 100644 --- a/src/public/app/widgets/note_title.js +++ b/src/public/app/widgets/note_title.js @@ -70,20 +70,38 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { } async refreshWithNote(note) { - const viewMode = this.noteContext.viewScope.viewMode; - this.$noteTitle.val(viewMode === 'default' - ? note.title - : `${viewMode}: ${note.title}`); + this.$noteTitle.val(await this.getTitleText(note)); this.$noteTitle.prop("readonly", (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) || ['_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(note.noteId) - || viewMode !== 'default' + || this.noteContext.viewScope.viewMode !== 'default' ); this.setProtectedStatus(note); } + /** @param {FNote} note */ + async getTitleText(note) { + const viewScope = this.noteContext.viewScope; + + let title = viewScope.viewMode === 'default' + ? note.title + : `${note.title}: ${viewScope.viewMode}`; + + if (viewScope.attachmentId) { + // assuming the attachment has been already loaded + const attachment = await note.getAttachmentById(viewScope.attachmentId); + + if (attachment) { + title += `: ${attachment.title}`; + } + } + + return title; + } + + /** @param {FNote} note */ setProtectedStatus(note) { this.$noteTitle.toggleClass("protected", !!note.isProtected); } diff --git a/src/public/app/widgets/type_widgets/attachment_detail.js b/src/public/app/widgets/type_widgets/attachment_detail.js new file mode 100644 index 000000000..8a6e60047 --- /dev/null +++ b/src/public/app/widgets/type_widgets/attachment_detail.js @@ -0,0 +1,54 @@ +import TypeWidget from "./type_widget.js"; +import server from "../../services/server.js"; +import AttachmentDetailWidget from "../attachment_detail.js"; + +const TPL = ` +
+ + +
+
`; + +export default class AttachmentDetailTypeWidget extends TypeWidget { + static getType() { + return "attachmentDetail"; + } + + doRender() { + this.$widget = $(TPL); + this.$wrapper = this.$widget.find('.attachment-wrapper'); + + super.doRender(); + } + + async doRefresh(note) { + this.$wrapper.empty(); + this.children = []; + this.renderedAttachmentIds = new Set(); + + const attachment = await server.get(`notes/${this.noteId}/attachments/${this.noteContext.viewScope.attachment.attachmentId}/?includeContent=true`); + + if (!attachment) { + this.$list.html("This attachment has been deleted."); + + return; + } + + const attachmentDetailWidget = new AttachmentDetailWidget(attachment); + this.child(attachmentDetailWidget); + + this.$list.append(attachmentDetailWidget.render()); + } + + async entitiesReloadedEvent({loadResults}) { + const attachmentChange = loadResults.getAttachments().find(att => att.attachmentId === this.attachment.attachmentId); + + if (attachmentChange.isDeleted) { + this.refresh(); // all other updates are handled within AttachmentDetailWidget + } + } +} diff --git a/src/public/app/widgets/type_widgets/attachments.js b/src/public/app/widgets/type_widgets/attachment_list.js similarity index 83% rename from src/public/app/widgets/type_widgets/attachments.js rename to src/public/app/widgets/type_widgets/attachment_list.js index b90d55409..90861b4ba 100644 --- a/src/public/app/widgets/type_widgets/attachments.js +++ b/src/public/app/widgets/type_widgets/attachment_list.js @@ -3,24 +3,24 @@ import server from "../../services/server.js"; import AttachmentDetailWidget from "../attachment_detail.js"; const TPL = ` -
+
-
+
`; -export default class AttachmentsTypeWidget extends TypeWidget { +export default class AttachmentListTypeWidget extends TypeWidget { static getType() { - return "attachments"; + return "attachmentList"; } doRender() { this.$widget = $(TPL); - this.$list = this.$widget.find('.attachment-list'); + this.$list = this.$widget.find('.attachment-list-wrapper'); super.doRender(); } diff --git a/src/routes/index.js b/src/routes/index.js index 7d01e08a5..c158a1842 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -34,8 +34,10 @@ function index(req, res) { instanceName: config.General ? config.General.instanceName : null, appCssNoteIds: getAppCssNoteIds(), isDev: env.isDev(), - isMainWindow: !req.query.extra, + isMainWindow: !req.query.extraWindow, extraHoistedNoteId: req.query.extraHoistedNoteId, + // make sure only valid JSON gets rendered + extraViewScope: JSON.stringify(req.query.extraViewScope ? JSON.parse(req.query.extraViewScope) : {}), isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(), maxContentWidth: parseInt(options.maxContentWidth), triliumVersion: packageJson.version, diff --git a/src/services/window.js b/src/services/window.js index a9180d519..e41e3d8f1 100644 --- a/src/services/window.js +++ b/src/services/window.js @@ -15,7 +15,7 @@ let mainWindow; /** @type {Electron.BrowserWindow} */ let setupWindow; -async function createExtraWindow(notePath, hoistedNoteId = 'root') { +async function createExtraWindow(notePath, hoistedNoteId = 'root', viewScope = {}) { const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled'); const {BrowserWindow} = require('electron'); @@ -35,13 +35,13 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') { }); win.setMenuBarVisibility(false); - win.loadURL(`http://127.0.0.1:${port}/?extra=1&extraHoistedNoteId=${hoistedNoteId}#${notePath}`); + win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1&extraHoistedNoteId=${hoistedNoteId}&extraViewScope=${JSON.stringify(viewScope)}#${notePath}`); configureWebContents(win.webContents, spellcheckEnabled); } ipcMain.on('create-extra-window', (event, arg) => { - createExtraWindow(arg.notePath, arg.hoistedNoteId); + createExtraWindow(arg.notePath, arg.hoistedNoteId, arg.viewScope); }); async function createMainWindow(app) { diff --git a/src/views/desktop.ejs b/src/views/desktop.ejs index f1e91b77d..fb9591792 100644 --- a/src/views/desktop.ejs +++ b/src/views/desktop.ejs @@ -33,6 +33,7 @@ appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>, isMainWindow: <%= isMainWindow %>, extraHoistedNoteId: '<%= extraHoistedNoteId %>', + extraViewScope: <%- extraViewScope %>, isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>, triliumVersion: "<%= triliumVersion %>", assetPath: "<%= assetPath %>",