diff --git a/src/becca/becca.js b/src/becca/becca.js
index 9bfa907ea..ee22d03b5 100644
--- a/src/becca/becca.js
+++ b/src/becca/becca.js
@@ -3,6 +3,7 @@
const sql = require("../services/sql");
const NoteSet = require("../services/search/note_set");
const BAttachment = require("./entities/battachment.js");
+const NotFoundError = require("../errors/not_found_error.js");
/**
* Becca is a backend cache of all notes, branches and attributes. There's a similar frontend cache Froca.
@@ -78,6 +79,16 @@ class Becca {
return this.notes[noteId];
}
+ /** @returns {BNote|null} */
+ getNoteOrThrow(noteId) {
+ const note = this.notes[noteId];
+ if (!note) {
+ throw new NotFoundError(`Note '${noteId}' doesn't exist.`);
+ }
+
+ return note;
+ }
+
/** @returns {BNote[]} */
getNotes(noteIds, ignoreMissing = false) {
const filteredNotes = [];
@@ -104,11 +115,30 @@ class Becca {
return this.branches[branchId];
}
+ /** @returns {BBranch|null} */
+ getBranchOrThrow(branchId) {
+ const branch = this.getBranch(branchId);
+ if (!branch) {
+ throw new NotFoundError(`Branch '${branchId}' was not found in becca.`);
+ }
+ return branch;
+ }
+
/** @returns {BAttribute|null} */
getAttribute(attributeId) {
return this.attributes[attributeId];
}
+ /** @returns {BAttribute} */
+ getAttributeOrThrow(attributeId) {
+ const attribute = this.getAttribute(attributeId);
+ if (!attribute) {
+ throw new NotFoundError(`Attribute '${attributeId}' does not exist.`);
+ }
+
+ return attribute;
+ }
+
/** @returns {BBranch|null} */
getBranchFromChildAndParent(childNoteId, parentNoteId) {
return this.childParentToBranch[`${childNoteId}-${parentNoteId}`];
@@ -130,6 +160,15 @@ class Becca {
return row ? new BAttachment(row) : null;
}
+ /** @returns {BAttachment} */
+ getAttachmentOrThrow(attachmentId) {
+ const attachment = this.getAttachment(attachmentId);
+ if (!attachment) {
+ throw new NotFoundError(`Attachment '${attachmentId}' has not been found.`);
+ }
+ return attachment;
+ }
+
/** @returns {BAttachment[]} */
getAttachments(attachmentIds) {
const BAttachment = require("./entities/battachment"); // avoiding circular dependency problems
diff --git a/src/becca/becca_loader.js b/src/becca/becca_loader.js
index fb9f6d3df..fc93d1932 100644
--- a/src/becca/becca_loader.js
+++ b/src/becca/becca_loader.js
@@ -27,7 +27,7 @@ function load() {
const start = Date.now();
becca.reset();
- // using raw query and passing arrays to avoid allocating new objects
+ // using a raw query and passing arrays to avoid allocating new objects,
// this is worth it for becca load since it happens every run and blocks the app until finished
for (const row of sql.getRawRows(`SELECT noteId, title, type, mime, isProtected, blobId, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes WHERE isDeleted = 0`)) {
diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js
index af68ca8d6..40bdbf308 100644
--- a/src/becca/entities/bnote.js
+++ b/src/becca/entities/bnote.js
@@ -11,7 +11,6 @@ const BAttachment = require("./battachment");
const TaskContext = require("../../services/task_context");
const dayjs = require("dayjs");
const utc = require('dayjs/plugin/utc');
-const NotFoundError = require("../../errors/not_found_error.js");
const eventService = require("../../services/events.js");
dayjs.extend(utc);
@@ -1622,11 +1621,7 @@ class BNote extends AbstractBeccaEntity {
let attachment;
if (attachmentId) {
- attachment = this.becca.getAttachment(attachmentId);
-
- if (!attachment) {
- throw new NotFoundError(`Attachment '${attachmentId}' has not been found.`);
- }
+ attachment = this.becca.getAttachmentOrThrow(attachmentId);
} else {
attachment = new BAttachment({
parentId: this.noteId,
diff --git a/src/public/app/services/import.js b/src/public/app/services/import.js
index 08a44920f..46548460c 100644
--- a/src/public/app/services/import.js
+++ b/src/public/app/services/import.js
@@ -4,7 +4,11 @@ import ws from "./ws.js";
import utils from "./utils.js";
import appContext from "../components/app_context.js";
-export async function uploadFiles(parentNoteId, files, options) {
+export async function uploadFiles(entityType, parentNoteId, files, options) {
+ if (!['notes', 'attachments'].includes(entityType)) {
+ throw new Error(`Unrecognized import entity type '${entityType}'.`);
+ }
+
if (files.length === 0) {
return;
}
@@ -25,7 +29,7 @@ export async function uploadFiles(parentNoteId, files, options) {
}
await $.ajax({
- url: `${window.glob.baseApiUrl}notes/${parentNoteId}/import`,
+ url: `${window.glob.baseApiUrl}notes/${parentNoteId}/${entityType}-import`,
headers: await server.getHeaders(),
data: formData,
dataType: 'json',
diff --git a/src/public/app/widgets/dialogs/import.js b/src/public/app/widgets/dialogs/import.js
index ddf86837a..0dff47e72 100644
--- a/src/public/app/widgets/dialogs/import.js
+++ b/src/public/app/widgets/dialogs/import.js
@@ -154,6 +154,6 @@ export default class ImportDialog extends BasicWidget {
this.$widget.modal('hide');
- await importService.uploadFiles(parentNoteId, files, options);
+ await importService.uploadFiles('notes', parentNoteId, files, options);
}
}
diff --git a/src/public/app/widgets/dialogs/upload_attachments.js b/src/public/app/widgets/dialogs/upload_attachments.js
new file mode 100644
index 000000000..f5d12a124
--- /dev/null
+++ b/src/public/app/widgets/dialogs/upload_attachments.js
@@ -0,0 +1,107 @@
+import utils from '../../services/utils.js';
+import treeService from "../../services/tree.js";
+import importService from "../../services/import.js";
+import options from "../../services/options.js";
+import BasicWidget from "../basic_widget.js";
+
+const TPL = `
+
`;
+
+export default class UploadAttachmentsDialog extends BasicWidget {
+ constructor() {
+ super();
+
+ this.parentNoteId = null;
+ }
+
+ doRender() {
+ this.$widget = $(TPL);
+ this.$form = this.$widget.find(".upload-attachment-form");
+ this.$noteTitle = this.$widget.find(".upload-attachment-note-title");
+ this.$fileUploadInput = this.$widget.find(".upload-attachment-file-upload-input");
+ this.$uploadButton = this.$widget.find(".upload-attachment-button");
+ this.$shrinkImagesCheckbox = this.$widget.find(".shrink-images-checkbox");
+
+ this.$form.on('submit', () => {
+ // disabling so that import is not triggered again.
+ this.$uploadButton.attr("disabled", "disabled");
+
+ this.uploadAttachments(this.parentNoteId);
+
+ return false;
+ });
+
+ this.$fileUploadInput.on('change', () => {
+ if (this.$fileUploadInput.val()) {
+ this.$uploadButton.removeAttr("disabled");
+ }
+ else {
+ this.$uploadButton.attr("disabled", "disabled");
+ }
+ });
+
+ this.$widget.find('[data-toggle="tooltip"]').tooltip({
+ html: true
+ });
+ }
+
+ async showUploadAttachmentsDialogEvent({noteId}) {
+ this.parentNoteId = noteId;
+
+ this.$fileUploadInput.val('').trigger('change'); // to trigger upload button disabling listener below
+ this.$shrinkImagesCheckbox.prop("checked", options.is('compressImages'));
+
+ this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId));
+
+ utils.openDialog(this.$widget);
+ }
+
+ async uploadAttachments(parentNoteId) {
+ const files = Array.from(this.$fileUploadInput[0].files); // shallow copy since we're resetting the upload button below
+
+ const boolToString = $el => $el.is(":checked") ? "true" : "false";
+
+ const options = {
+ shrinkImages: boolToString(this.$shrinkImagesCheckbox),
+ };
+
+ this.$widget.modal('hide');
+
+ await importService.uploadFiles('attachments', parentNoteId, files, options);
+ }
+}
diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js
index 920d3ce10..bee29e998 100644
--- a/src/public/app/widgets/note_detail.js
+++ b/src/public/app/widgets/note_detail.js
@@ -116,7 +116,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
const importService = await import('../services/import.js');
- importService.uploadFiles(activeNote.noteId, files, {
+ importService.uploadFiles('notes', activeNote.noteId, files, {
safeImport: true,
shrinkImages: true,
textImportedAsText: true,
diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js
index 8688abe6c..4cff2a955 100644
--- a/src/public/app/widgets/note_tree.js
+++ b/src/public/app/widgets/note_tree.js
@@ -442,7 +442,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const importService = await import('../services/import.js');
- importService.uploadFiles(node.data.noteId, files, {
+ importService.uploadFiles('notes', node.data.noteId, files, {
safeImport: true,
shrinkImages: true,
textImportedAsText: true,
diff --git a/src/public/app/widgets/type_widgets/attachment_list.js b/src/public/app/widgets/type_widgets/attachment_list.js
index 4399a43ee..58a584851 100644
--- a/src/public/app/widgets/type_widgets/attachment_list.js
+++ b/src/public/app/widgets/type_widgets/attachment_list.js
@@ -37,7 +37,10 @@ export default class AttachmentListTypeWidget extends TypeWidget {
async doRefresh(note) {
this.$linksWrapper.append(
"Owning note: ",
- await linkService.createNoteLink(this.noteId)
+ await linkService.createNoteLink(this.noteId),
+ $('