mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
attachment actions
This commit is contained in:
parent
d232694dec
commit
d8bc9c2982
@ -136,6 +136,7 @@ class AbstractBeccaEntity {
|
||||
// client code asks to save entity even if blobId didn't change (something else was changed)
|
||||
opts.forceSave = !!opts.forceSave;
|
||||
opts.forceCold = !!opts.forceCold;
|
||||
opts.forceFrontendReload = !!opts.forceFrontendReload;
|
||||
|
||||
if (content === null || content === undefined) {
|
||||
throw new Error(`Cannot set null content to ${this.constructor.primaryKeyName} '${this[this.constructor.primaryKeyName]}'`);
|
||||
@ -176,7 +177,7 @@ class AbstractBeccaEntity {
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_saveBlob(content, unencryptedContentForHashCalculation, opts) {
|
||||
_saveBlob(content, unencryptedContentForHashCalculation, opts = {}) {
|
||||
let newBlobId;
|
||||
let blobNeedsInsert;
|
||||
|
||||
@ -212,7 +213,10 @@ class AbstractBeccaEntity {
|
||||
hash: hash,
|
||||
isErased: false,
|
||||
utcDateChanged: pojo.utcDateModified,
|
||||
isSynced: true
|
||||
isSynced: true,
|
||||
// overriding componentId will cause frontend to think the change is coming from a different component
|
||||
// and thus reload
|
||||
componentId: opts.forceFrontendReload ? utils.randomString(10) : null
|
||||
});
|
||||
|
||||
eventService.emit(eventService.ENTITY_CHANGED, {
|
||||
|
@ -96,6 +96,7 @@ class BAttachment extends AbstractBeccaEntity {
|
||||
* @param {object} [opts]
|
||||
* @param {object} [opts.forceSave=false] - will also save this BAttachment entity
|
||||
* @param {object} [opts.forceCold=false] - blob has to be saved as cold
|
||||
* @param {object} [opts.forceFrontendReload=false] - override frontend heuristics on when to reload, instruct to reload
|
||||
*/
|
||||
setContent(content, opts) {
|
||||
this._setContent(content, opts);
|
||||
@ -154,6 +155,12 @@ class BAttachment extends AbstractBeccaEntity {
|
||||
return { note, branch };
|
||||
}
|
||||
|
||||
getFileName() {
|
||||
const type = this.role === 'image' ? 'image' : 'file';
|
||||
|
||||
return utils.formatDownloadTitle(this.title, type, this.mime);
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
|
@ -242,6 +242,7 @@ class BNote extends AbstractBeccaEntity {
|
||||
* @param {object} [opts]
|
||||
* @param {object} [opts.forceSave=false] - will also save this BNote entity
|
||||
* @param {object} [opts.forceCold=false] - blob has to be saved as cold
|
||||
* @param {object} [opts.forceFrontendReload=false] - override frontend heuristics on when to reload, instruct to reload
|
||||
*/
|
||||
setContent(content, opts) {
|
||||
this._setContent(content, opts);
|
||||
@ -1642,6 +1643,10 @@ class BNote extends AbstractBeccaEntity {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
getFileName() {
|
||||
return utils.formatDownloadTitle(this.title, this.type, this.mime);
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
|
@ -22,7 +22,7 @@ import NoteIconWidget from "../widgets/note_icon.js";
|
||||
import SearchResultWidget from "../widgets/search_result.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import NoteUpdateStatusWidget from "../widgets/note_update_status.js";
|
||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||
import SpacerWidget from "../widgets/spacer.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
@ -150,7 +150,7 @@ export default class DesktopLayout {
|
||||
.button(new NoteActionsWidget())
|
||||
)
|
||||
.child(new SharedInfoWidget())
|
||||
.child(new NoteUpdateStatusWidget())
|
||||
.child(new WatchedFileUpdateStatusWidget())
|
||||
.child(new FloatingButtons()
|
||||
.child(new EditButton())
|
||||
.child(new CodeButtonsWidget())
|
||||
|
@ -1,18 +1,33 @@
|
||||
import ws from "./ws.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
|
||||
const fileModificationStatus = {};
|
||||
const fileModificationStatus = {
|
||||
notes: {},
|
||||
attachments: {}
|
||||
};
|
||||
|
||||
function getFileModificationStatus(noteId) {
|
||||
return fileModificationStatus[noteId];
|
||||
function checkType(type) {
|
||||
if (type !== 'notes' && type !== 'attachments') {
|
||||
throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`);
|
||||
}
|
||||
}
|
||||
|
||||
function fileModificationUploaded(noteId) {
|
||||
delete fileModificationStatus[noteId];
|
||||
function getFileModificationStatus(entityType, entityId) {
|
||||
checkType(entityType);
|
||||
|
||||
return fileModificationStatus[entityType][entityId];
|
||||
}
|
||||
|
||||
function ignoreModification(noteId) {
|
||||
delete fileModificationStatus[noteId];
|
||||
function fileModificationUploaded(entityType, entityId) {
|
||||
checkType(entityType);
|
||||
|
||||
delete fileModificationStatus[entityType][entityId];
|
||||
}
|
||||
|
||||
function ignoreModification(entityType, entityId) {
|
||||
checkType(entityType);
|
||||
|
||||
delete fileModificationStatus[entityType][entityId];
|
||||
}
|
||||
|
||||
ws.subscribeToMessages(async message => {
|
||||
@ -20,10 +35,13 @@ ws.subscribeToMessages(async message => {
|
||||
return;
|
||||
}
|
||||
|
||||
fileModificationStatus[message.noteId] = message;
|
||||
checkType(message.entityType);
|
||||
|
||||
fileModificationStatus[message.entityType][message.entityId] = message;
|
||||
|
||||
appContext.triggerEvent('openedFileUpdated', {
|
||||
noteId: message.noteId,
|
||||
entityType: message.entityType,
|
||||
entityId: message.entityId,
|
||||
lastModifiedMs: message.lastModifiedMs,
|
||||
filePath: message.filePath
|
||||
});
|
||||
|
@ -39,7 +39,7 @@ async function processEntityChanges(entityChanges) {
|
||||
// NOOP
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unknown entityName ${ec.entityName}`);
|
||||
throw new Error(`Unknown entityName '${ec.entityName}'`);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
@ -92,7 +92,7 @@ function processNoteChange(loadResults, ec) {
|
||||
loadResults.addNote(ec.entityId, ec.componentId);
|
||||
|
||||
if (ec.isErased && ec.entityId in froca.notes) {
|
||||
utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
|
||||
utils.reloadFrontendApp(`${ec.entityName} '${ec.entityId}' is erased, need to do complete reload.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ function processNoteChange(loadResults, ec) {
|
||||
|
||||
async function processBranchChange(loadResults, ec) {
|
||||
if (ec.isErased && ec.entityId in froca.branches) {
|
||||
utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
|
||||
utils.reloadFrontendApp(`${ec.entityName} '${ec.entityId}' is erased, need to do complete reload.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -192,7 +192,7 @@ function processAttributeChange(loadResults, ec) {
|
||||
let attribute = froca.attributes[ec.entityId];
|
||||
|
||||
if (ec.isErased && ec.entityId in froca.attributes) {
|
||||
utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
|
||||
utils.reloadFrontendApp(`${ec.entityName} '${ec.entityId}' is erased, need to do complete reload.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -241,7 +241,7 @@ 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.`);
|
||||
utils.reloadFrontendApp(`${ec.entityName} '${ec.entityId}' is erased, need to do complete reload.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
32
src/public/app/services/image.js
Normal file
32
src/public/app/services/image.js
Normal file
@ -0,0 +1,32 @@
|
||||
import toastService from "./toast.js";
|
||||
|
||||
function copyImageReferenceToClipboard($imageWrapper) {
|
||||
try {
|
||||
$imageWrapper.attr('contenteditable', 'true');
|
||||
selectImage($imageWrapper.get(0));
|
||||
|
||||
const success = document.execCommand('copy');
|
||||
|
||||
if (success) {
|
||||
toastService.showMessage("Image copied to the clipboard");
|
||||
} else {
|
||||
toastService.showAndLogError("Could not copy the image to clipboard.");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
window.getSelection().removeAllRanges();
|
||||
$imageWrapper.removeAttr('contenteditable');
|
||||
}
|
||||
}
|
||||
|
||||
function selectImage(element) {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
export default {
|
||||
copyImageReferenceToClipboard
|
||||
};
|
@ -125,8 +125,6 @@ function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) {
|
||||
hash += `?${paramStr}`;
|
||||
}
|
||||
|
||||
console.log(hash);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,22 @@
|
||||
import utils from "./utils.js";
|
||||
import server from "./server.js";
|
||||
|
||||
function getFileUrl(noteId) {
|
||||
return getUrlForDownload(`api/notes/${noteId}/download`);
|
||||
function checkType(type) {
|
||||
if (type !== 'notes' && type !== 'attachments') {
|
||||
throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`);
|
||||
}
|
||||
}
|
||||
function getOpenFileUrl(noteId) {
|
||||
return getUrlForDownload(`api/notes/${noteId}/open`);
|
||||
|
||||
function getFileUrl(type, noteId) {
|
||||
checkType(type);
|
||||
|
||||
return getUrlForDownload(`api/${type}/${noteId}/download`);
|
||||
}
|
||||
|
||||
function getOpenFileUrl(type, noteId) {
|
||||
checkType(type);
|
||||
|
||||
return getUrlForDownload(`api/${type}/${noteId}/open`);
|
||||
}
|
||||
|
||||
function download(url) {
|
||||
@ -19,32 +30,15 @@ function download(url) {
|
||||
}
|
||||
|
||||
function downloadFileNote(noteId) {
|
||||
const url = `${getFileUrl(noteId)}?${Date.now()}`; // don't use cache
|
||||
const url = `${getFileUrl('notes', noteId)}?${Date.now()}`; // don't use cache
|
||||
|
||||
download(url);
|
||||
}
|
||||
|
||||
async function openNoteExternally(noteId, mime) {
|
||||
if (utils.isElectron()) {
|
||||
const resp = await server.post(`notes/${noteId}/save-to-tmp-dir`);
|
||||
function downloadAttachment(attachmentId) {
|
||||
const url = `${getFileUrl('attachments', attachmentId)}?${Date.now()}`; // don't use cache
|
||||
|
||||
const electron = utils.dynamicRequire('electron');
|
||||
const res = await electron.shell.openPath(resp.tmpFilePath);
|
||||
|
||||
if (res) {
|
||||
// fallback in case there's no default application for this file
|
||||
open(getFileUrl(noteId), {url: true});
|
||||
}
|
||||
}
|
||||
else {
|
||||
// allow browser to handle opening common file
|
||||
if (mime === "application/pdf" || mime.startsWith("image") || mime.startsWith("audio") || mime.startsWith("video")){
|
||||
window.open(getOpenFileUrl(noteId));
|
||||
}
|
||||
else {
|
||||
window.location.href = getFileUrl(noteId);
|
||||
}
|
||||
}
|
||||
download(url);
|
||||
}
|
||||
|
||||
function downloadNoteRevision(noteId, noteRevisionId) {
|
||||
@ -67,6 +61,40 @@ function getUrlForDownload(url) {
|
||||
}
|
||||
}
|
||||
|
||||
function canOpenInBrowser(mime) {
|
||||
return mime === "application/pdf"
|
||||
|| mime.startsWith("image")
|
||||
|| mime.startsWith("audio")
|
||||
|| mime.startsWith("video");
|
||||
}
|
||||
|
||||
async function openExternally(type, entityId, mime) {
|
||||
checkType(type);
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const resp = await server.post(`${type}/${entityId}/save-to-tmp-dir`);
|
||||
|
||||
const electron = utils.dynamicRequire('electron');
|
||||
const res = await electron.shell.openPath(resp.tmpFilePath);
|
||||
|
||||
if (res) {
|
||||
// fallback in case there's no default application for this file
|
||||
window.open(getFileUrl(type, entityId), { url: true });
|
||||
}
|
||||
}
|
||||
else {
|
||||
// allow browser to handle opening common file
|
||||
if (canOpenInBrowser(mime)) {
|
||||
window.open(getOpenFileUrl(type, entityId));
|
||||
} else {
|
||||
window.location.href = getFileUrl(type, entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const openNoteExternally = async (noteId, mime) => await openExternally('notes', noteId, mime);
|
||||
const openAttachmentExternally = async (attachmentId, mime) => await openExternally('attachments', attachmentId, mime);
|
||||
|
||||
function getHost() {
|
||||
const url = new URL(window.location.href);
|
||||
return `${url.protocol}//${url.hostname}:${url.port}`;
|
||||
@ -75,7 +103,9 @@ function getHost() {
|
||||
export default {
|
||||
download,
|
||||
downloadFileNote,
|
||||
openNoteExternally,
|
||||
downloadNoteRevision,
|
||||
getUrlForDownload
|
||||
downloadAttachment,
|
||||
getUrlForDownload,
|
||||
openNoteExternally,
|
||||
openAttachmentExternally,
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import AttachmentActionsWidget from "./buttons/attachments_actions.js";
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
import server from "../services/server.js";
|
||||
import options from "../services/options.js";
|
||||
import imageService from "../services/image.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="attachment-detail">
|
||||
@ -148,6 +149,10 @@ export default class AttachmentDetailWidget extends BasicWidget {
|
||||
}
|
||||
}
|
||||
|
||||
copyAttachmentReferenceToClipboard() {
|
||||
imageService.copyImageReferenceToClipboard(this.$wrapper.find('.attachment-content'));
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({loadResults}) {
|
||||
const attachmentChange = loadResults.getAttachments().find(att => att.attachmentId === this.attachment.attachmentId);
|
||||
|
||||
|
@ -4,6 +4,7 @@ import dialogService from "../../services/dialog.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import ws from "../../services/ws.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import openService from "../../services/open.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="dropdown attachment-actions">
|
||||
@ -28,8 +29,15 @@ const TPL = `
|
||||
aria-expanded="false" class="icon-action icon-action-always-border bx bx-dots-vertical-rounded"></button>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a data-trigger-command="deleteAttachment" class="dropdown-item">Delete attachment</a>
|
||||
<a data-trigger-command="openAttachment" class="dropdown-item">Open</a>
|
||||
<a data-trigger-command="openAttachmentExternally" class="dropdown-item"
|
||||
title="File will be open in an external application and watched for changes. You'll then be able to upload the modified version back to Trilium.">
|
||||
Open externally</a>
|
||||
<a data-trigger-command="downloadAttachment" class="dropdown-item">Download</a>
|
||||
<a data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item">Upload new revision</a>
|
||||
<a data-trigger-command="copyAttachmentReferenceToClipboard" class="dropdown-item">Copy reference to clipboard</a>
|
||||
<a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item">Convert attachment into note</a>
|
||||
<a data-trigger-command="deleteAttachment" class="dropdown-item">Delete attachment</a>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@ -40,9 +48,30 @@ export default class AttachmentActionsWidget extends BasicWidget {
|
||||
this.attachment = attachment;
|
||||
}
|
||||
|
||||
get attachmentId() {
|
||||
return this.attachment.attachmentId;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
|
||||
this.$widget.find("[data-trigger-command='copyAttachmentReferenceToClipboard']").toggle(this.attachment.role === 'image');
|
||||
}
|
||||
|
||||
async openAttachmentCommand() {
|
||||
await openService.openAttachmentExternally(this.attachmentId, this.attachment.mime);
|
||||
}
|
||||
|
||||
async downloadAttachmentCommand() {
|
||||
await openService.downloadAttachment(this.attachmentId);
|
||||
}
|
||||
|
||||
async copyAttachmentReferenceToClipboardCommand() {
|
||||
this.parent.copyAttachmentReferenceToClipboard();
|
||||
}
|
||||
|
||||
async openAttachmentExternallyCommand() {
|
||||
await openService.openAttachmentExternally(this.attachmentId, this.attachment.mime);
|
||||
}
|
||||
|
||||
async deleteAttachmentCommand() {
|
||||
@ -50,7 +79,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
await server.remove(`attachments/${this.attachment.attachmentId}`);
|
||||
await server.remove(`attachments/${this.attachmentId}`);
|
||||
toastService.showMessage(`Attachment '${this.attachment.title}' has been deleted.`);
|
||||
}
|
||||
|
||||
@ -59,7 +88,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
const {note: newNote} = await server.post(`attachments/${this.attachment.attachmentId}/convert-to-note`)
|
||||
const {note: newNote} = await server.post(`attachments/${this.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);
|
||||
|
@ -35,7 +35,11 @@ const TPL = `
|
||||
<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="showAttachments" class="dropdown-item"><kbd data-command="showAttachments"></kbd> Note attachments</a>
|
||||
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"><kbd data-command="openNoteExternally"></kbd> Open note externally</a>
|
||||
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"
|
||||
title="File will be open in an external application and watched for changes. You'll then be able to upload the modified version back to Trilium.">
|
||||
<kbd data-command="openNoteExternally"></kbd>
|
||||
Open note externally
|
||||
</a>
|
||||
<a class="dropdown-item import-files-button">Import files</a>
|
||||
<a class="dropdown-item export-note-button">Export note</a>
|
||||
<a class="dropdown-item delete-note-button">Delete note</a>
|
||||
|
@ -1,64 +0,0 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import server from "../services/server.js";
|
||||
import fileWatcher from "../services/file_watcher.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="dropdown note-update-status-widget alert alert-warning">
|
||||
<style>
|
||||
.note-update-status-widget {
|
||||
margin: 10px;
|
||||
contain: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<p>File <code class="file-path"></code> has been last modified on <span class="file-last-modified"></span>.</p>
|
||||
|
||||
<div style="display: flex; flex-direction: row; justify-content: space-evenly;">
|
||||
<button class="btn btn-sm file-upload-button">Upload modified file</button>
|
||||
|
||||
<button class="btn btn-sm ignore-this-change-button">Ignore this change</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class NoteUpdateStatusWidget extends NoteContextAwareWidget {
|
||||
isEnabled() {
|
||||
return super.isEnabled()
|
||||
&& !!fileWatcher.getFileModificationStatus(this.noteId);
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$filePath = this.$widget.find(".file-path");
|
||||
this.$fileLastModified = this.$widget.find(".file-last-modified");
|
||||
this.$fileUploadButton = this.$widget.find(".file-upload-button");
|
||||
|
||||
this.$fileUploadButton.on("click", async () => {
|
||||
await server.post(`notes/${this.noteId}/upload-modified-file`, {
|
||||
filePath: this.$filePath.text()
|
||||
});
|
||||
|
||||
fileWatcher.fileModificationUploaded(this.noteId);
|
||||
this.refresh();
|
||||
});
|
||||
|
||||
this.$ignoreThisChangeButton = this.$widget.find(".ignore-this-change-button");
|
||||
this.$ignoreThisChangeButton.on('click', () => {
|
||||
fileWatcher.ignoreModification(this.noteId);
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
refreshWithNote(note) {
|
||||
const status = fileWatcher.getFileModificationStatus(note.noteId);
|
||||
|
||||
this.$filePath.text(status.filePath);
|
||||
this.$fileLastModified.text(dayjs.unix(status.lastModifiedMs / 1000).format("HH:mm:ss"));
|
||||
}
|
||||
|
||||
openedFileUpdatedEvent(data) {
|
||||
if (data.noteId === this.noteId) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
@ -69,7 +69,7 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
|
||||
this.$fileSize = this.$widget.find(".image-filesize");
|
||||
|
||||
this.$openButton = this.$widget.find(".image-open");
|
||||
this.$openButton.on('click', () => openService.openNoteExternally(this.noteId, this.note.mime ));
|
||||
this.$openButton.on('click', () => openService.openNoteExternally(this.noteId, this.note.mime));
|
||||
|
||||
this.$imageDownloadButton = this.$widget.find(".image-download");
|
||||
this.$imageDownloadButton.on('click', () => openService.downloadFileNote(this.noteId));
|
||||
|
@ -1,8 +1,8 @@
|
||||
import utils from "../../services/utils.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import libraryLoader from "../../services/library_loader.js";
|
||||
import contextMenu from "../../menus/context_menu.js";
|
||||
import imageService from "../../services/image.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail-image note-detail-printable">
|
||||
@ -73,7 +73,7 @@ class ImageTypeWidget extends TypeWidget {
|
||||
],
|
||||
selectMenuItemHandler: ({command}) => {
|
||||
if (command === 'copyImageReferenceToClipboard') {
|
||||
this.copyImageReferenceToClipboard();
|
||||
imageService.copyImageReferenceToClipboard(this.$imageWrapper);
|
||||
} else if (command === 'copyImageToClipboard') {
|
||||
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
|
||||
utils.dynamicRequire('electron');
|
||||
@ -98,36 +98,7 @@ class ImageTypeWidget extends TypeWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
this.copyImageReferenceToClipboard();
|
||||
}
|
||||
|
||||
copyImageReferenceToClipboard() {
|
||||
this.$imageWrapper.attr('contenteditable','true');
|
||||
|
||||
try {
|
||||
this.selectImage(this.$imageWrapper.get(0));
|
||||
|
||||
const success = document.execCommand('copy');
|
||||
|
||||
if (success) {
|
||||
toastService.showMessage("Image copied to the clipboard");
|
||||
}
|
||||
else {
|
||||
toastService.showAndLogError("Could not copy the image to clipboard.");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
window.getSelection().removeAllRanges();
|
||||
this.$imageWrapper.removeAttr('contenteditable');
|
||||
}
|
||||
}
|
||||
|
||||
selectImage(element) {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
imageService.copyImageReferenceToClipboard(this.$imageWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
|
96
src/public/app/widgets/watched_file_update_status.js
Normal file
96
src/public/app/widgets/watched_file_update_status.js
Normal file
@ -0,0 +1,96 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import server from "../services/server.js";
|
||||
import fileWatcher from "../services/file_watcher.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="dropdown watched-file-update-status-widget alert alert-warning">
|
||||
<style>
|
||||
.watched-file-update-status-widget {
|
||||
margin: 10px;
|
||||
contain: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<p>File <code class="file-path"></code> has been last modified on <span class="file-last-modified"></span>.</p>
|
||||
|
||||
<div style="display: flex; flex-direction: row; justify-content: space-evenly;">
|
||||
<button class="btn btn-sm file-upload-button">Upload modified file</button>
|
||||
|
||||
<button class="btn btn-sm ignore-this-change-button">Ignore this change</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class WatchedFileUpdateStatusWidget extends NoteContextAwareWidget {
|
||||
isEnabled() {
|
||||
const { entityType, entityId } = this.getEntity();
|
||||
|
||||
console.log(entityType, entityId);
|
||||
|
||||
return super.isEnabled() && !!fileWatcher.getFileModificationStatus(entityType, entityId);
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$filePath = this.$widget.find(".file-path");
|
||||
this.$fileLastModified = this.$widget.find(".file-last-modified");
|
||||
this.$fileUploadButton = this.$widget.find(".file-upload-button");
|
||||
|
||||
this.$fileUploadButton.on("click", async () => {
|
||||
const { entityType, entityId } = this.getEntity();
|
||||
|
||||
await server.post(`${entityType}/${entityId}/upload-modified-file`, {
|
||||
filePath: this.$filePath.text()
|
||||
});
|
||||
|
||||
fileWatcher.fileModificationUploaded(entityType, entityId);
|
||||
this.refresh();
|
||||
});
|
||||
|
||||
this.$ignoreThisChangeButton = this.$widget.find(".ignore-this-change-button");
|
||||
this.$ignoreThisChangeButton.on('click', () => {
|
||||
const { entityType, entityId } = this.getEntity();
|
||||
|
||||
fileWatcher.ignoreModification(entityType, entityId);
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
refreshWithNote(note) {
|
||||
const { entityType, entityId } = this.getEntity();
|
||||
const status = fileWatcher.getFileModificationStatus(entityType, entityId);
|
||||
|
||||
console.log("status", status);
|
||||
|
||||
this.$filePath.text(status.filePath);
|
||||
this.$fileLastModified.text(dayjs.unix(status.lastModifiedMs / 1000).format("HH:mm:ss"));
|
||||
}
|
||||
|
||||
getEntity() {
|
||||
if (!this.noteContext) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { viewScope } = this.noteContext;
|
||||
|
||||
if (viewScope.viewMode === 'attachments' && viewScope.attachmentId) {
|
||||
return {
|
||||
entityType: 'attachments',
|
||||
entityId: viewScope.attachmentId
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
entityType: 'notes',
|
||||
entityId: this.noteId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
openedFileUpdatedEvent(data) {console.log(data);
|
||||
const { entityType, entityId } = this.getEntity();
|
||||
|
||||
if (data.entityType === entityType && data.entityId === entityId) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ const chokidar = require('chokidar');
|
||||
const ws = require('../../services/ws');
|
||||
const becca = require("../../becca/becca");
|
||||
const NotFoundError = require("../../errors/not_found_error");
|
||||
const ValidationError = require("../../errors/validation_error.js");
|
||||
|
||||
function updateFile(req) {
|
||||
const {noteId} = req.params;
|
||||
@ -38,61 +39,84 @@ function updateFile(req) {
|
||||
};
|
||||
}
|
||||
|
||||
function getFilename(note) {
|
||||
// (one) reason we're not using the originFileName (available as label) is that it's not
|
||||
// available for older note revisions and thus would be inconsistent
|
||||
return utils.formatDownloadTitle(note.title, note.type, note.mime);
|
||||
/**
|
||||
* @param {BNote|BAttachment} noteOrAttachment
|
||||
* @param res
|
||||
* @param {boolean} contentDisposition
|
||||
*/
|
||||
function downloadData(noteOrAttachment, res, contentDisposition) {
|
||||
if (noteOrAttachment.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
|
||||
return res.status(401).send("Protected session not available");
|
||||
}
|
||||
|
||||
if (contentDisposition) {
|
||||
const fileName = noteOrAttachment.getFileName();
|
||||
|
||||
res.setHeader('Content-Disposition', utils.getContentDisposition(fileName));
|
||||
}
|
||||
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader('Content-Type', noteOrAttachment.mime);
|
||||
|
||||
res.send(noteOrAttachment.getContent());
|
||||
}
|
||||
|
||||
function downloadNoteFile(noteId, res, contentDisposition = true) {
|
||||
function downloadNoteInt(noteId, res, contentDisposition = true) {
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return res.setHeader("Content-Type", "text/plain")
|
||||
.status(404)
|
||||
.send(`Note ${noteId} doesn't exist.`);
|
||||
.send(`Note '${noteId}' doesn't exist.`);
|
||||
}
|
||||
|
||||
if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
|
||||
return res.status(401).send("Protected session not available");
|
||||
return downloadData(note, res, contentDisposition);
|
||||
}
|
||||
|
||||
function downloadAttachmentInt(attachmentId, res, contentDisposition = true) {
|
||||
const attachment = becca.getAttachment(attachmentId);
|
||||
|
||||
if (!attachment) {
|
||||
return res.setHeader("Content-Type", "text/plain")
|
||||
.status(404)
|
||||
.send(`Attachment '${attachmentId}' doesn't exist.`);
|
||||
}
|
||||
|
||||
if (contentDisposition) {
|
||||
const filename = getFilename(note);
|
||||
|
||||
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
|
||||
}
|
||||
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader('Content-Type', note.mime);
|
||||
|
||||
res.send(note.getContent());
|
||||
return downloadData(attachment, res, contentDisposition);
|
||||
}
|
||||
|
||||
function downloadFile(req, res) {
|
||||
const noteId = req.params.noteId;
|
||||
const downloadFile = (req, res) => downloadNoteInt(req.params.noteId, res, true);
|
||||
const openFile = (req, res) => downloadNoteInt(req.params.noteId, res, false);
|
||||
|
||||
return downloadNoteFile(noteId, res);
|
||||
}
|
||||
|
||||
function openFile(req, res) {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
return downloadNoteFile(noteId, res, false);
|
||||
}
|
||||
const downloadAttachment = (req, res) => downloadAttachmentInt(req.params.attachmentId, res, true);
|
||||
const openAttachment = (req, res) => downloadAttachmentInt(req.params.attachmentId, res, false);
|
||||
|
||||
function fileContentProvider(req) {
|
||||
// Read file name from route params.
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
const fileName = getFilename(note);
|
||||
let content = note.getContent();
|
||||
if (!note) {
|
||||
throw new NotFoundError(`Note '${req.params.noteId}' doesn't exist.`);
|
||||
}
|
||||
|
||||
return streamContent(note.getContent(), note.getFileName(), note.mime);
|
||||
}
|
||||
|
||||
function attachmentContentProvider(req) {
|
||||
// Read file name from route params.
|
||||
const attachment = becca.getAttachment(req.params.attachmentId);
|
||||
if (!attachment) {
|
||||
throw new NotFoundError(`Attachment '${req.params.attachmentId}' doesn't exist.`);
|
||||
}
|
||||
|
||||
return streamContent(attachment.getContent(), attachment.getFileName(), attachment.mime);
|
||||
}
|
||||
|
||||
function streamContent(content, fileName, mimeType) {
|
||||
if (typeof content === "string") {
|
||||
content = Buffer.from(content, 'utf8');
|
||||
content = Buffer.from(content, 'utf8');
|
||||
}
|
||||
|
||||
const totalSize = content.byteLength;
|
||||
const mimeType = note.mime;
|
||||
|
||||
const getStream = range => {
|
||||
if (!range) {
|
||||
@ -100,7 +124,7 @@ function fileContentProvider(req) {
|
||||
return Readable.from(content);
|
||||
}
|
||||
// Partial content request.
|
||||
const { start, end } = range;
|
||||
const {start, end} = range;
|
||||
|
||||
return Readable.from(content.slice(start, end + 1));
|
||||
}
|
||||
@ -113,27 +137,44 @@ function fileContentProvider(req) {
|
||||
};
|
||||
}
|
||||
|
||||
function saveToTmpDir(req) {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
function saveNoteToTmpDir(req) {
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
if (!note) {
|
||||
throw new NotFoundError(`Note '${noteId}' doesn't exist.`);
|
||||
throw new NotFoundError(`Note '${req.params.noteId}' doesn't exist.`);
|
||||
}
|
||||
|
||||
const tmpObj = tmp.fileSync({postfix: getFilename(note)});
|
||||
const fileName = note.getFileName();
|
||||
const content = note.getContent();
|
||||
|
||||
fs.writeSync(tmpObj.fd, note.getContent());
|
||||
return saveToTmpDir(fileName, content, 'notes', note.noteId);
|
||||
}
|
||||
|
||||
function saveAttachmentToTmpDir(req) {
|
||||
const attachment = becca.getAttachment(req.params.attachmentId);
|
||||
if (!attachment) {
|
||||
throw new NotFoundError(`Attachment '${req.params.attachmentId}' doesn't exist.`);
|
||||
}
|
||||
|
||||
const fileName = attachment.getFileName();
|
||||
const content = attachment.getContent();
|
||||
|
||||
return saveToTmpDir(fileName, content, 'attachments', attachment.attachmentId);
|
||||
}
|
||||
|
||||
function saveToTmpDir(fileName, content, entityType, entityId) {
|
||||
const tmpObj = tmp.fileSync({ postfix: fileName });
|
||||
|
||||
fs.writeSync(tmpObj.fd, content);
|
||||
fs.closeSync(tmpObj.fd);
|
||||
|
||||
log.info(`Saved temporary file for note ${noteId} into ${tmpObj.name}`);
|
||||
log.info(`Saved temporary file ${tmpObj.name}`);
|
||||
|
||||
if (utils.isElectron()) {
|
||||
chokidar.watch(tmpObj.name).on('change', (path, stats) => {
|
||||
ws.sendMessageToAllClients({
|
||||
type: 'openedFileUpdated',
|
||||
noteId: noteId,
|
||||
entityType: entityType,
|
||||
entityId: entityId,
|
||||
lastModifiedMs: stats.atimeMs,
|
||||
filePath: tmpObj.name
|
||||
});
|
||||
@ -145,11 +186,63 @@ function saveToTmpDir(req) {
|
||||
};
|
||||
}
|
||||
|
||||
function uploadModifiedFileToNote(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const {filePath} = req.body;
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
throw new NotFoundError(`Note '${noteId}' has not been found`);
|
||||
}
|
||||
|
||||
log.info(`Updating note '${noteId}' with content from '${filePath}'`);
|
||||
|
||||
note.saveNoteRevision();
|
||||
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
|
||||
if (!fileContent) {
|
||||
throw new ValidationError(`File '${fileContent}' is empty`);
|
||||
}
|
||||
|
||||
note.setContent(fileContent);
|
||||
}
|
||||
|
||||
function uploadModifiedFileToAttachment(req) {
|
||||
const {attachmentId} = req.params;
|
||||
const {filePath} = req.body;
|
||||
|
||||
const attachment = becca.getAttachment(attachmentId);
|
||||
|
||||
if (!attachment) {
|
||||
throw new NotFoundError(`Attachment '${attachmentId}' has not been found`);
|
||||
}
|
||||
|
||||
log.info(`Updating attachment '${attachmentId}' with content from '${filePath}'`);
|
||||
|
||||
attachment.getNote().saveNoteRevision();
|
||||
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
|
||||
if (!fileContent) {
|
||||
throw new ValidationError(`File '${fileContent}' is empty`);
|
||||
}
|
||||
|
||||
attachment.setContent(fileContent);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
updateFile,
|
||||
openFile,
|
||||
fileContentProvider,
|
||||
downloadFile,
|
||||
downloadNoteFile,
|
||||
saveToTmpDir
|
||||
downloadNoteInt,
|
||||
saveNoteToTmpDir,
|
||||
openAttachment,
|
||||
downloadAttachment,
|
||||
saveAttachmentToTmpDir,
|
||||
attachmentContentProvider,
|
||||
uploadModifiedFileToNote,
|
||||
uploadModifiedFileToAttachment
|
||||
};
|
||||
|
@ -229,29 +229,6 @@ function getDeleteNotesPreview(req) {
|
||||
};
|
||||
}
|
||||
|
||||
function uploadModifiedFile(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const {filePath} = req.body;
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
throw new NotFoundError(`Note '${noteId}' has not been found`);
|
||||
}
|
||||
|
||||
log.info(`Updating note '${noteId}' with content from ${filePath}`);
|
||||
|
||||
note.saveNoteRevision();
|
||||
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
|
||||
if (!fileContent) {
|
||||
throw new ValidationError(`File '${fileContent}' is empty`);
|
||||
}
|
||||
|
||||
note.setContent(fileContent);
|
||||
}
|
||||
|
||||
function forceSaveNoteRevision(req) {
|
||||
const {noteId} = req.params;
|
||||
const note = becca.getNote(noteId);
|
||||
@ -294,7 +271,6 @@ module.exports = {
|
||||
eraseDeletedNotesNow,
|
||||
eraseUnusedAttachmentsNow,
|
||||
getDeleteNotesPreview,
|
||||
uploadModifiedFile,
|
||||
forceSaveNoteRevision,
|
||||
convertNoteToAttachment
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
const log = require('../services/log');
|
||||
const fileUploadService = require('./api/files');
|
||||
const fileService = require('./api/files');
|
||||
const scriptService = require('../services/script');
|
||||
const cls = require('../services/cls');
|
||||
const sql = require("../services/sql");
|
||||
@ -26,7 +26,7 @@ function handleRequest(req, res) {
|
||||
match = path.match(regex);
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`Testing path for label ${attr.attributeId}, regex=${attr.value} failed with error ${e.stack}`);
|
||||
log.error(`Testing path for label '${attr.attributeId}', regex '${attr.value}' failed with error: ${e.message}, stack: ${e.stack}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ function handleRequest(req, res) {
|
||||
if (attr.name === 'customRequestHandler') {
|
||||
const note = attr.getNote();
|
||||
|
||||
log.info(`Handling custom request "${path}" with note ${note.noteId}`);
|
||||
log.info(`Handling custom request '${path}' with note '${note.noteId}'`);
|
||||
|
||||
try {
|
||||
scriptService.executeNote(note, {
|
||||
@ -47,7 +47,7 @@ function handleRequest(req, res) {
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`Custom handler ${note.noteId} failed with ${e.message}`);
|
||||
log.error(`Custom handler '${note.noteId}' failed with: ${e.message}, ${e.stack}`);
|
||||
|
||||
res.setHeader("Content-Type", "text/plain")
|
||||
.status(500)
|
||||
@ -55,16 +55,16 @@ function handleRequest(req, res) {
|
||||
}
|
||||
}
|
||||
else if (attr.name === 'customResourceProvider') {
|
||||
fileUploadService.downloadNoteFile(attr.noteId, res);
|
||||
fileService.downloadNoteInt(attr.noteId, res);
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unrecognized attribute name ${attr.name}`);
|
||||
throw new Error(`Unrecognized attribute name '${attr.name}'`);
|
||||
}
|
||||
|
||||
return; // only first handler is executed
|
||||
}
|
||||
|
||||
const message = `No handler matched for custom ${path} request.`;
|
||||
const message = `No handler matched for custom '${path}' request.`;
|
||||
|
||||
log.info(message);
|
||||
res.setHeader("Content-Type", "text/plain")
|
||||
|
@ -122,7 +122,6 @@ function register(app) {
|
||||
apiRoute(PUT, '/api/notes/:noteId/type', notesApiRoute.setNoteTypeMime);
|
||||
apiRoute(PUT, '/api/notes/:noteId/title', notesApiRoute.changeTitle);
|
||||
apiRoute(PST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree);
|
||||
apiRoute(PST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile);
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch);
|
||||
apiRoute(PUT, '/api/notes/:noteId/toggle-in-parent/:parentNoteId/:present', cloningApiRoute.toggleNoteInParent);
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToParentNote);
|
||||
@ -137,7 +136,8 @@ function register(app) {
|
||||
route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
|
||||
// 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/save-to-tmp-dir', filesRoute.saveNoteToTmpDir);
|
||||
apiRoute(PST, '/api/notes/:noteId/upload-modified-file', filesRoute.uploadModifiedFileToNote);
|
||||
apiRoute(PST, '/api/notes/:noteId/convert-to-attachment', notesApiRoute.convertNoteToAttachment);
|
||||
|
||||
apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent);
|
||||
@ -154,6 +154,16 @@ function register(app) {
|
||||
apiRoute(PST, '/api/attachments/:attachmentId/convert-to-note', attachmentsApiRoute.convertAttachmentToNote);
|
||||
apiRoute(DEL, '/api/attachments/:attachmentId', attachmentsApiRoute.deleteAttachment);
|
||||
route(GET, '/api/attachments/:attachmentId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage);
|
||||
route(GET, '/api/attachments/:attachmentId/open', [auth.checkApiAuthOrElectron], filesRoute.openAttachment);
|
||||
route(GET, '/api/attachments/:attachmentId/open-partial', [auth.checkApiAuthOrElectron],
|
||||
createPartialContentHandler(filesRoute.attachmentContentProvider, {
|
||||
debug: (string, extra) => { console.log(string, extra); }
|
||||
}));
|
||||
route(GET, '/api/attachments/:attachmentId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadAttachment);
|
||||
// this "hacky" path is used for easier referencing of CSS resources
|
||||
route(GET, '/api/attachments/download/:attachmentId', [auth.checkApiAuthOrElectron], filesRoute.downloadAttachment);
|
||||
apiRoute(PST, '/api/attachments/:attachmentId/save-to-tmp-dir', filesRoute.saveAttachmentToTmpDir);
|
||||
apiRoute(PST, '/api/attachments/:attachmentId/upload-modified-file', filesRoute.uploadModifiedFileToAttachment);
|
||||
|
||||
apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
|
||||
apiRoute(DEL, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions);
|
||||
|
@ -362,8 +362,9 @@ function checkImageAttachments(note, content) {
|
||||
|
||||
const existingAttachmentIds = new Set(imageAttachments.map(att => att.attachmentId));
|
||||
const unknownAttachmentIds = Array.from(foundAttachmentIds).filter(foundAttId => !existingAttachmentIds.has(foundAttId));
|
||||
const unknownAttachments = becca.getAttachments(unknownAttachmentIds);
|
||||
|
||||
for (const unknownAttachment of 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;
|
||||
@ -374,7 +375,10 @@ function checkImageAttachments(note, content) {
|
||||
log.info(`Copied attachment '${unknownAttachment.attachmentId}' to new '${newAttachment.attachmentId}'`);
|
||||
}
|
||||
|
||||
return content;
|
||||
return {
|
||||
forceFrontendReload: unknownAttachments.length > 0,
|
||||
content
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -591,6 +595,7 @@ function saveLinks(note, content) {
|
||||
}
|
||||
|
||||
const foundLinks = [];
|
||||
let forceFrontendReload = false;
|
||||
|
||||
if (note.type === 'text') {
|
||||
content = downloadImages(note.noteId, content);
|
||||
@ -599,7 +604,7 @@ function saveLinks(note, content) {
|
||||
content = findInternalLinks(content, foundLinks);
|
||||
content = findIncludeNoteLinks(content, foundLinks);
|
||||
|
||||
content = checkImageAttachments(note, content);
|
||||
({forceFrontendReload, content} = checkImageAttachments(note, content));
|
||||
}
|
||||
else if (note.type === 'relationMap') {
|
||||
findRelationMapLinks(content, foundLinks);
|
||||
@ -643,7 +648,7 @@ function saveLinks(note, content) {
|
||||
unusedLink.markAsDeleted();
|
||||
}
|
||||
|
||||
return content;
|
||||
return { forceFrontendReload, content };
|
||||
}
|
||||
|
||||
/** @param {BNote} note */
|
||||
@ -677,9 +682,9 @@ function updateNoteData(noteId, content) {
|
||||
|
||||
saveNoteRevisionIfNeeded(note);
|
||||
|
||||
content = saveLinks(note, content);
|
||||
const { forceFrontendReload, content: newContent } = saveLinks(note, content);
|
||||
|
||||
note.setContent(content);
|
||||
note.setContent(newContent, { forceFrontendReload });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -780,15 +785,15 @@ function scanForLinks(note, content) {
|
||||
|
||||
try {
|
||||
sql.transactional(() => {
|
||||
const newContent = saveLinks(note, content);
|
||||
const { forceFrontendReload, content: newContent } = saveLinks(note, content);
|
||||
|
||||
if (content !== newContent) {
|
||||
note.setContent(newContent);
|
||||
note.setContent(newContent, { forceFrontendReload });
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`Could not scan for links note ${note.noteId}: ${e.message} ${e.stack}`);
|
||||
log.error(`Could not scan for links note '${note.noteId}': ${e.message} ${e.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,33 +199,33 @@ function replaceAll(string, replaceWhat, replaceWith) {
|
||||
return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith);
|
||||
}
|
||||
|
||||
function formatDownloadTitle(filename, type, mime) {
|
||||
if (!filename) {
|
||||
filename = "untitled";
|
||||
function formatDownloadTitle(fileName, type, mime) {
|
||||
if (!fileName) {
|
||||
fileName = "untitled";
|
||||
}
|
||||
|
||||
filename = sanitize(filename);
|
||||
fileName = sanitize(fileName);
|
||||
|
||||
if (type === 'text') {
|
||||
return `${filename}.html`;
|
||||
return `${fileName}.html`;
|
||||
} else if (['relationMap', 'canvas', 'search'].includes(type)) {
|
||||
return `${filename}.json`;
|
||||
return `${fileName}.json`;
|
||||
} else {
|
||||
if (!mime) {
|
||||
return filename;
|
||||
return fileName;
|
||||
}
|
||||
|
||||
mime = mime.toLowerCase();
|
||||
const filenameLc = filename.toLowerCase();
|
||||
const filenameLc = fileName.toLowerCase();
|
||||
const extensions = mimeTypes.extensions[mime];
|
||||
|
||||
if (!extensions || extensions.length === 0) {
|
||||
return filename;
|
||||
return fileName;
|
||||
}
|
||||
|
||||
for (const ext of extensions) {
|
||||
if (filenameLc.endsWith(`.${ext}`)) {
|
||||
return filename;
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,10 +234,10 @@ function formatDownloadTitle(filename, type, mime) {
|
||||
// the current name without fake extension. It's possible that the title still preserves to correct
|
||||
// extension too
|
||||
|
||||
return filename;
|
||||
return fileName;
|
||||
}
|
||||
|
||||
return `${filename}.${extensions[0]}`;
|
||||
return `${fileName}.${extensions[0]}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user