From 626af84f42e3944ab5406a0a05cb384cb431326d Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 8 May 2023 00:02:08 +0200 Subject: [PATCH] refactoring + uploading attachments WIP --- src/becca/becca.js | 39 +++++++ src/becca/becca_loader.js | 2 +- src/becca/entities/bnote.js | 7 +- src/public/app/services/import.js | 8 +- src/public/app/widgets/dialogs/import.js | 2 +- .../app/widgets/dialogs/upload_attachments.js | 107 ++++++++++++++++++ src/public/app/widgets/note_detail.js | 2 +- src/public/app/widgets/note_tree.js | 2 +- .../widgets/type_widgets/attachment_list.js | 5 +- src/routes/api/attachments.js | 29 +---- src/routes/api/attributes.js | 7 +- src/routes/api/branches.js | 19 +--- src/routes/api/files.js | 46 ++------ src/routes/api/image.js | 13 +-- src/routes/api/import.js | 69 +++++++++-- src/routes/api/note_map.js | 21 +--- src/routes/api/notes.js | 32 +----- src/routes/api/search.js | 13 +-- src/routes/api/similar_notes.js | 7 +- src/routes/api/sql.js | 7 +- src/routes/api/stats.js | 8 +- src/routes/routes.js | 3 +- src/services/image.js | 8 +- 23 files changed, 257 insertions(+), 199 deletions(-) create mode 100644 src/public/app/widgets/dialogs/upload_attachments.js 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), + $('