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 = `
@@ -16,11 +19,11 @@ const TPL = `
width: 35px;
height: 35px;
}
-
+
.attachment-actions .dropdown-menu {
width: 20em;
}
-
+
.attachment-actions .dropdown-item .bx {
position: relative;
top: 3px;
@@ -35,7 +38,7 @@ const TPL = `
}
-
@@ -43,17 +46,17 @@ const TPL = `
${t("attachments_actions.open_externally")}
-
+
${t("attachments_actions.open_custom")}
-
+
${t("attachments_actions.download")}
${t("attachments_actions.copy_link_to_clipboard")}
-
+
@@ -68,18 +71,23 @@ const TPL = `
-
+
${t("attachments_actions.convert_attachment_into_note")}
-
+
-
+
`;
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 = $(`