diff --git a/src/becca/entities/rows.ts b/src/becca/entities/rows.ts index ba9189190..b57801e1e 100644 --- a/src/becca/entities/rows.ts +++ b/src/becca/entities/rows.ts @@ -1,4 +1,5 @@ // TODO: Booleans should probably be numbers instead (as SQLite does not have booleans.); +// TODO: check against schema.sql which properties really are "optional" export interface AttachmentRow { attachmentId?: string; @@ -12,6 +13,8 @@ export interface AttachmentRow { dateModified?: string; utcDateModified?: string; utcDateScheduledForErasureSince?: string; + isDeleted?: boolean; + deleteId?: string; contentLength?: number; content?: Buffer | string; } diff --git a/src/public/app/entities/fattachment.ts b/src/public/app/entities/fattachment.ts index 274c1bbdb..4774f87bd 100644 --- a/src/public/app/entities/fattachment.ts +++ b/src/public/app/entities/fattachment.ts @@ -19,18 +19,18 @@ export interface FAttachmentRow { class FAttachment { private froca: Froca; attachmentId!: string; - private ownerId!: string; + ownerId!: string; role!: string; mime!: string; title!: string; isProtected!: boolean; // TODO: Is this used? private dateModified!: string; utcDateModified!: string; - private utcDateScheduledForErasureSince!: string; + utcDateScheduledForErasureSince!: string; /** * optionally added to the entity */ - private contentLength!: number; + contentLength!: number; constructor(froca: Froca, row: FAttachmentRow) { /** @type {Froca} */ diff --git a/src/public/app/services/content_renderer.ts b/src/public/app/services/content_renderer.ts index 18aafdb4f..7064829fc 100644 --- a/src/public/app/services/content_renderer.ts +++ b/src/public/app/services/content_renderer.ts @@ -24,7 +24,8 @@ interface Options { const CODE_MIME_TYPES = new Set(["application/json"]); -async function getRenderedContent(this: {} | { ctx: string }, entity: FNote, options: Options = {}) { +async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) { + options = Object.assign( { tooltip: false @@ -47,7 +48,7 @@ async function getRenderedContent(this: {} | { ctx: string }, entity: FNote, opt renderFile(entity, type, $renderedContent); } else if (type === "mermaid") { await renderMermaid(entity, $renderedContent); - } else if (type === "render") { + } else if (type === "render" && entity instanceof FNote) { const $content = $("
"); await renderService.render(entity, $content); @@ -79,7 +80,7 @@ async function getRenderedContent(this: {} | { ctx: string }, entity: FNote, opt }; } -async function renderText(note: FNote, $renderedContent: JQuery) { +async function renderText(note: FNote | FAttachment, $renderedContent: JQuery) { // entity must be FNote const blob = await note.getBlob(); @@ -102,7 +103,7 @@ async function renderText(note: FNote, $renderedContent: JQuery) { } await applySyntaxHighlight($renderedContent); - } else { + } else if (note instanceof FNote) { await renderChildrenList($renderedContent, note); } } @@ -110,7 +111,7 @@ async function renderText(note: FNote, $renderedContent: JQuery) { /** * Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type. */ -async function renderCode(note: FNote, $renderedContent: JQuery) { +async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery) { const blob = await note.getBlob(); const $codeBlock = $(""); @@ -208,7 +209,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: $renderedContent.append($content); } -async function renderMermaid(note: FNote, $renderedContent: JQuery) { +async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery) { await libraryLoader.requireLibrary(libraryLoader.MERMAID); const blob = await note.getBlob(); diff --git a/src/public/app/services/link.ts b/src/public/app/services/link.ts index 472f7d37a..9b3c779bb 100644 --- a/src/public/app/services/link.ts +++ b/src/public/app/services/link.ts @@ -70,7 +70,7 @@ interface CreateLinkOptions { viewScope?: ViewScope; } -async function createLink(notePath: string, options: CreateLinkOptions = {}) { +async function createLink(notePath: string | undefined, options: CreateLinkOptions = {}) { if (!notePath || !notePath.trim()) { logError("Missing note path"); diff --git a/src/public/app/services/load_results.ts b/src/public/app/services/load_results.ts index 25f3b30ec..eabbcfc74 100644 --- a/src/public/app/services/load_results.ts +++ b/src/public/app/services/load_results.ts @@ -1,4 +1,4 @@ -import type { TaskRow } from "../../../becca/entities/rows.js"; +import type { TaskRow, AttachmentRow } from "../../../becca/entities/rows.js"; import type { AttributeType } from "../entities/fattribute.js"; import type { EntityChange } from "../server_types.js"; @@ -37,8 +37,6 @@ interface ContentNoteIdToComponentIdRow { componentId: string; } -interface AttachmentRow {} - interface OptionRow {} interface NoteReorderingRow {} diff --git a/src/public/app/widgets/attachment_detail.js b/src/public/app/widgets/attachment_detail.ts similarity index 92% rename from src/public/app/widgets/attachment_detail.js rename to src/public/app/widgets/attachment_detail.ts index 8aa6b5824..a66ae95a1 100644 --- a/src/public/app/widgets/attachment_detail.js +++ b/src/public/app/widgets/attachment_detail.ts @@ -7,6 +7,8 @@ import imageService from "../services/image.js"; import linkService from "../services/link.js"; import contentRenderer from "../services/content_renderer.js"; import toastService from "../services/toast.js"; +import type FAttachment from "../entities/fattachment.js"; +import type { EventData } from "../components/app_context.js"; const TPL = `
@@ -96,7 +98,12 @@ const TPL = `
`; export default class AttachmentDetailWidget extends BasicWidget { - constructor(attachment, isFullDetail) { + attachment: FAttachment; + attachmentActionsWidget: AttachmentActionsWidget; + isFullDetail: boolean; + $wrapper!: JQuery; + + constructor(attachment: FAttachment, isFullDetail: boolean) { super(); this.contentSized(); @@ -140,7 +147,8 @@ export default class AttachmentDetailWidget extends BasicWidget { this.$wrapper.addClass("scheduled-for-deletion"); const scheduledSinceTimestamp = utils.parseDate(utcDateScheduledForErasureSince)?.getTime(); - const intervalMs = options.getInt("eraseUnusedAttachmentsAfterSeconds") * 1000; + // use default value (30 days in seconds) from options_init as fallback, in case getInt returns null + const intervalMs = options.getInt("eraseUnusedAttachmentsAfterSeconds") || 2592000 * 1000; const deletionTimestamp = scheduledSinceTimestamp + intervalMs; const willBeDeletedInMs = deletionTimestamp - Date.now(); @@ -185,7 +193,7 @@ export default class AttachmentDetailWidget extends BasicWidget { } } - async entitiesReloadedEvent({ loadResults }) { + async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { const attachmentRow = loadResults.getAttachmentRows().find((att) => att.attachmentId === this.attachment.attachmentId); if (attachmentRow) { diff --git a/src/public/app/widgets/buttons/attachments_actions.js b/src/public/app/widgets/buttons/attachments_actions.ts similarity index 85% rename from src/public/app/widgets/buttons/attachments_actions.js rename to src/public/app/widgets/buttons/attachments_actions.ts index 5ee51b9d9..efacf48fe 100644 --- a/src/public/app/widgets/buttons/attachments_actions.js +++ b/src/public/app/widgets/buttons/attachments_actions.ts @@ -8,6 +8,9 @@ import appContext from "../../components/app_context.js"; import openService from "../../services/open.js"; import utils from "../../services/utils.js"; import { Dropdown } from "bootstrap"; +import type attachmentsApiRoute from "../../../../routes/api/attachments.js" +import type FAttachment from "../../entities/fattachment.js"; +import type AttachmentDetailWidget from "../attachment_detail.js"; const TPL = ` - +
`; export default class AttachmentActionsWidget extends BasicWidget { - constructor(attachment, isFullDetail) { + $uploadNewRevisionInput!: JQuery; + attachment: FAttachment; + isFullDetail: boolean; + dropdown!: Dropdown; + + constructor(attachment: FAttachment, isFullDetail: boolean) { super(); this.attachment = attachment; @@ -92,20 +100,21 @@ export default class AttachmentActionsWidget extends BasicWidget { doRender() { this.$widget = $(TPL); - this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")); + this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]); this.$widget.on("click", ".dropdown-item", () => this.dropdown.toggle()); this.$uploadNewRevisionInput = this.$widget.find(".attachment-upload-new-revision-input"); this.$uploadNewRevisionInput.on("change", async () => { - const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below + + const fileToUpload = this.$uploadNewRevisionInput[0].files?.item(0); // copy to allow reset below this.$uploadNewRevisionInput.val(""); - - const result = await server.upload(`attachments/${this.attachmentId}/file`, fileToUpload); - - if (result.uploaded) { - toastService.showMessage(t("attachments_actions.upload_success")); - } else { - toastService.showError(t("attachments_actions.upload_failed")); + if (fileToUpload) { + const result = await server.upload(`attachments/${this.attachmentId}/file`, fileToUpload); + if (result.uploaded) { + toastService.showMessage(t("attachments_actions.upload_success")); + } else { + toastService.showError(t("attachments_actions.upload_failed")); + } } }); @@ -122,6 +131,7 @@ export default class AttachmentActionsWidget extends BasicWidget { const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']"); $openAttachmentCustomButton.addClass("disabled").append($('').attr("title", t("attachments_actions.open_custom_client_only"))); } + } async openAttachmentCommand() { @@ -141,7 +151,9 @@ export default class AttachmentActionsWidget extends BasicWidget { } async copyAttachmentLinkToClipboardCommand() { - this.parent.copyAttachmentLinkToClipboard(); + if (this.parent && "copyAttachmentLinkToClipboard" in this.parent) { + (this.parent as AttachmentDetailWidget).copyAttachmentLinkToClipboard(); + } } async deleteAttachmentCommand() { @@ -158,7 +170,8 @@ export default class AttachmentActionsWidget extends BasicWidget { return; } - const { note: newNote } = await server.post(`attachments/${this.attachmentId}/convert-to-note`); + + const { note: newNote } = await server.post>(`attachments/${this.attachmentId}/convert-to-note`); toastService.showMessage(t("attachments_actions.convert_success", { title: this.attachment.title })); await ws.waitForMaxKnownEntityChangeId(); await appContext.tabManager.getActiveContext().setNote(newNote.noteId); diff --git a/src/public/app/widgets/type_widgets/attachment_detail.js b/src/public/app/widgets/type_widgets/attachment_detail.ts similarity index 84% rename from src/public/app/widgets/type_widgets/attachment_detail.js rename to src/public/app/widgets/type_widgets/attachment_detail.ts index 108c5da2e..5fc8e4201 100644 --- a/src/public/app/widgets/type_widgets/attachment_detail.js +++ b/src/public/app/widgets/type_widgets/attachment_detail.ts @@ -4,6 +4,8 @@ import linkService from "../../services/link.js"; import froca from "../../services/froca.js"; import utils from "../../services/utils.js"; import { t } from "../../services/i18n.js"; +import type FNote from "../../entities/fnote.js"; +import type { EventData } from "../../components/app_context.js"; const TPL = `
@@ -32,6 +34,9 @@ const TPL = `
`; export default class AttachmentDetailTypeWidget extends TypeWidget { + $wrapper!: JQuery; + $linksWrapper!: JQuery; + static getType() { return "attachmentDetail"; } @@ -44,7 +49,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget { super.doRender(); } - async doRefresh(note) { + async doRefresh(note: Parameters[0]) { this.$wrapper.empty(); this.children = []; @@ -69,7 +74,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget { $helpButton ); - const attachment = await froca.getAttachment(this.attachmentId, true); + const attachment = (this.attachmentId) ? await froca.getAttachment(this.attachmentId, true) : null; if (!attachment) { this.$wrapper.html("" + t("attachment_detail.attachment_deleted") + ""); @@ -82,7 +87,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget { this.$wrapper.append(attachmentDetailWidget.render()); } - async entitiesReloadedEvent({ loadResults }) { + async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { const attachmentRow = loadResults.getAttachmentRows().find((att) => att.attachmentId === this.attachmentId); if (attachmentRow?.isDeleted) { @@ -91,6 +96,6 @@ export default class AttachmentDetailTypeWidget extends TypeWidget { } get attachmentId() { - return this.noteContext.viewScope.attachmentId; + return this?.noteContext?.viewScope?.attachmentId; } } diff --git a/src/public/app/widgets/type_widgets/attachment_list.js b/src/public/app/widgets/type_widgets/attachment_list.ts similarity index 79% rename from src/public/app/widgets/type_widgets/attachment_list.js rename to src/public/app/widgets/type_widgets/attachment_list.ts index f6eadb97e..3018f784b 100644 --- a/src/public/app/widgets/type_widgets/attachment_list.js +++ b/src/public/app/widgets/type_widgets/attachment_list.ts @@ -3,6 +3,7 @@ import AttachmentDetailWidget from "../attachment_detail.js"; import linkService from "../../services/link.js"; import utils from "../../services/utils.js"; import { t } from "../../services/i18n.js"; +import type { EventData } from "../../components/app_context.js"; const TPL = `
@@ -27,6 +28,10 @@ const TPL = `
`; export default class AttachmentListTypeWidget extends TypeWidget { + $list!: JQuery; + $linksWrapper!: JQuery; + renderedAttachmentIds!: Set; + static getType() { return "attachmentList"; } @@ -39,7 +44,10 @@ export default class AttachmentListTypeWidget extends TypeWidget { super.doRender(); } - async doRefresh(note) { + async doRefresh(note: Parameters[0]) { + // TriliumNextTODO: do we need to handle an undefined/null note? + if (!note) return false; + const $helpButton = $(`