Merge pull request #1260 from TriliumNext/chore_port-to-ts_attach

chore(ts): port various attachment related files to TS
This commit is contained in:
Elian Doran 2025-02-23 18:33:41 +02:00 committed by GitHub
commit 937a314260
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 85 additions and 45 deletions

View File

@ -1,4 +1,5 @@
// TODO: Booleans should probably be numbers instead (as SQLite does not have booleans.); // 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 { export interface AttachmentRow {
attachmentId?: string; attachmentId?: string;
@ -12,6 +13,8 @@ export interface AttachmentRow {
dateModified?: string; dateModified?: string;
utcDateModified?: string; utcDateModified?: string;
utcDateScheduledForErasureSince?: string; utcDateScheduledForErasureSince?: string;
isDeleted?: boolean;
deleteId?: string;
contentLength?: number; contentLength?: number;
content?: Buffer | string; content?: Buffer | string;
} }

View File

@ -19,18 +19,18 @@ export interface FAttachmentRow {
class FAttachment { class FAttachment {
private froca: Froca; private froca: Froca;
attachmentId!: string; attachmentId!: string;
private ownerId!: string; ownerId!: string;
role!: string; role!: string;
mime!: string; mime!: string;
title!: string; title!: string;
isProtected!: boolean; // TODO: Is this used? isProtected!: boolean; // TODO: Is this used?
private dateModified!: string; private dateModified!: string;
utcDateModified!: string; utcDateModified!: string;
private utcDateScheduledForErasureSince!: string; utcDateScheduledForErasureSince!: string;
/** /**
* optionally added to the entity * optionally added to the entity
*/ */
private contentLength!: number; contentLength!: number;
constructor(froca: Froca, row: FAttachmentRow) { constructor(froca: Froca, row: FAttachmentRow) {
/** @type {Froca} */ /** @type {Froca} */

View File

@ -24,7 +24,8 @@ interface Options {
const CODE_MIME_TYPES = new Set(["application/json"]); 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( options = Object.assign(
{ {
tooltip: false tooltip: false
@ -47,7 +48,7 @@ async function getRenderedContent(this: {} | { ctx: string }, entity: FNote, opt
renderFile(entity, type, $renderedContent); renderFile(entity, type, $renderedContent);
} else if (type === "mermaid") { } else if (type === "mermaid") {
await renderMermaid(entity, $renderedContent); await renderMermaid(entity, $renderedContent);
} else if (type === "render") { } else if (type === "render" && entity instanceof FNote) {
const $content = $("<div>"); const $content = $("<div>");
await renderService.render(entity, $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<HTMLElement>) { async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
// entity must be FNote // entity must be FNote
const blob = await note.getBlob(); const blob = await note.getBlob();
@ -102,7 +103,7 @@ async function renderText(note: FNote, $renderedContent: JQuery<HTMLElement>) {
} }
await applySyntaxHighlight($renderedContent); await applySyntaxHighlight($renderedContent);
} else { } else if (note instanceof FNote) {
await renderChildrenList($renderedContent, note); await renderChildrenList($renderedContent, note);
} }
} }
@ -110,7 +111,7 @@ async function renderText(note: FNote, $renderedContent: JQuery<HTMLElement>) {
/** /**
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type. * 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<HTMLElement>) { async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
const blob = await note.getBlob(); const blob = await note.getBlob();
const $codeBlock = $("<code>"); const $codeBlock = $("<code>");
@ -208,7 +209,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
$renderedContent.append($content); $renderedContent.append($content);
} }
async function renderMermaid(note: FNote, $renderedContent: JQuery<HTMLElement>) { async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
await libraryLoader.requireLibrary(libraryLoader.MERMAID); await libraryLoader.requireLibrary(libraryLoader.MERMAID);
const blob = await note.getBlob(); const blob = await note.getBlob();

View File

@ -70,7 +70,7 @@ interface CreateLinkOptions {
viewScope?: ViewScope; viewScope?: ViewScope;
} }
async function createLink(notePath: string, options: CreateLinkOptions = {}) { async function createLink(notePath: string | undefined, options: CreateLinkOptions = {}) {
if (!notePath || !notePath.trim()) { if (!notePath || !notePath.trim()) {
logError("Missing note path"); logError("Missing note path");

View File

@ -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 { AttributeType } from "../entities/fattribute.js";
import type { EntityChange } from "../server_types.js"; import type { EntityChange } from "../server_types.js";
@ -37,8 +37,6 @@ interface ContentNoteIdToComponentIdRow {
componentId: string; componentId: string;
} }
interface AttachmentRow {}
interface OptionRow {} interface OptionRow {}
interface NoteReorderingRow {} interface NoteReorderingRow {}

View File

@ -7,6 +7,8 @@ import imageService from "../services/image.js";
import linkService from "../services/link.js"; import linkService from "../services/link.js";
import contentRenderer from "../services/content_renderer.js"; import contentRenderer from "../services/content_renderer.js";
import toastService from "../services/toast.js"; import toastService from "../services/toast.js";
import type FAttachment from "../entities/fattachment.js";
import type { EventData } from "../components/app_context.js";
const TPL = ` const TPL = `
<div class="attachment-detail-widget"> <div class="attachment-detail-widget">
@ -96,7 +98,12 @@ const TPL = `
</div>`; </div>`;
export default class AttachmentDetailWidget extends BasicWidget { export default class AttachmentDetailWidget extends BasicWidget {
constructor(attachment, isFullDetail) { attachment: FAttachment;
attachmentActionsWidget: AttachmentActionsWidget;
isFullDetail: boolean;
$wrapper!: JQuery<HTMLElement>;
constructor(attachment: FAttachment, isFullDetail: boolean) {
super(); super();
this.contentSized(); this.contentSized();
@ -140,7 +147,8 @@ export default class AttachmentDetailWidget extends BasicWidget {
this.$wrapper.addClass("scheduled-for-deletion"); this.$wrapper.addClass("scheduled-for-deletion");
const scheduledSinceTimestamp = utils.parseDate(utcDateScheduledForErasureSince)?.getTime(); 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 deletionTimestamp = scheduledSinceTimestamp + intervalMs;
const willBeDeletedInMs = deletionTimestamp - Date.now(); 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); const attachmentRow = loadResults.getAttachmentRows().find((att) => att.attachmentId === this.attachment.attachmentId);
if (attachmentRow) { if (attachmentRow) {

View File

@ -8,6 +8,9 @@ import appContext from "../../components/app_context.js";
import openService from "../../services/open.js"; import openService from "../../services/open.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import { Dropdown } from "bootstrap"; 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 = ` const TPL = `
<div class="dropdown attachment-actions"> <div class="dropdown attachment-actions">
@ -16,11 +19,11 @@ const TPL = `
width: 35px; width: 35px;
height: 35px; height: 35px;
} }
.attachment-actions .dropdown-menu { .attachment-actions .dropdown-menu {
width: 20em; width: 20em;
} }
.attachment-actions .dropdown-item .bx { .attachment-actions .dropdown-item .bx {
position: relative; position: relative;
top: 3px; top: 3px;
@ -35,7 +38,7 @@ const TPL = `
} }
</style> </style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" <button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" class="icon-action icon-action-always-border bx bx-dots-vertical-rounded" aria-expanded="false" class="icon-action icon-action-always-border bx bx-dots-vertical-rounded"
style="position: relative; top: 3px;"></button> style="position: relative; top: 3px;"></button>
@ -43,17 +46,17 @@ const TPL = `
<li data-trigger-command="openAttachment" class="dropdown-item" <li data-trigger-command="openAttachment" class="dropdown-item"
title="${t("attachments_actions.open_externally_title")}"><span class="bx bx-file-find"></span> ${t("attachments_actions.open_externally")}</li> title="${t("attachments_actions.open_externally_title")}"><span class="bx bx-file-find"></span> ${t("attachments_actions.open_externally")}</li>
<li data-trigger-command="openAttachmentCustom" class="dropdown-item" <li data-trigger-command="openAttachmentCustom" class="dropdown-item"
title="${t("attachments_actions.open_custom_title")}"><span class="bx bx-customize"></span> ${t("attachments_actions.open_custom")}</li> title="${t("attachments_actions.open_custom_title")}"><span class="bx bx-customize"></span> ${t("attachments_actions.open_custom")}</li>
<li data-trigger-command="downloadAttachment" class="dropdown-item"> <li data-trigger-command="downloadAttachment" class="dropdown-item">
<span class="bx bx-download"></span> ${t("attachments_actions.download")}</li> <span class="bx bx-download"></span> ${t("attachments_actions.download")}</li>
<li data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item"><span class="bx bx-link"> <li data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item"><span class="bx bx-link">
</span> ${t("attachments_actions.copy_link_to_clipboard")}</li> </span> ${t("attachments_actions.copy_link_to_clipboard")}</li>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
@ -68,18 +71,23 @@ const TPL = `
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<li data-trigger-command="convertAttachmentIntoNote" class="dropdown-item"><span class="bx bx-note"> <li data-trigger-command="convertAttachmentIntoNote" class="dropdown-item"><span class="bx bx-note">
</span> ${t("attachments_actions.convert_attachment_into_note")}</li> </span> ${t("attachments_actions.convert_attachment_into_note")}</li>
</div> </div>
<input type="file" class="attachment-upload-new-revision-input" style="display: none"> <input type="file" class="attachment-upload-new-revision-input" style="display: none">
</div>`; </div>`;
export default class AttachmentActionsWidget extends BasicWidget { export default class AttachmentActionsWidget extends BasicWidget {
constructor(attachment, isFullDetail) { $uploadNewRevisionInput!: JQuery<HTMLInputElement>;
attachment: FAttachment;
isFullDetail: boolean;
dropdown!: Dropdown;
constructor(attachment: FAttachment, isFullDetail: boolean) {
super(); super();
this.attachment = attachment; this.attachment = attachment;
@ -92,20 +100,21 @@ export default class AttachmentActionsWidget extends BasicWidget {
doRender() { doRender() {
this.$widget = $(TPL); 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.$widget.on("click", ".dropdown-item", () => this.dropdown.toggle());
this.$uploadNewRevisionInput = this.$widget.find(".attachment-upload-new-revision-input"); this.$uploadNewRevisionInput = this.$widget.find(".attachment-upload-new-revision-input");
this.$uploadNewRevisionInput.on("change", async () => { 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(""); this.$uploadNewRevisionInput.val("");
if (fileToUpload) {
const result = await server.upload(`attachments/${this.attachmentId}/file`, fileToUpload); const result = await server.upload(`attachments/${this.attachmentId}/file`, fileToUpload);
if (result.uploaded) {
if (result.uploaded) { toastService.showMessage(t("attachments_actions.upload_success"));
toastService.showMessage(t("attachments_actions.upload_success")); } else {
} else { toastService.showError(t("attachments_actions.upload_failed"));
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']"); const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton.addClass("disabled").append($('<span class="bx bx-info-circle disabled-tooltip" />').attr("title", t("attachments_actions.open_custom_client_only"))); $openAttachmentCustomButton.addClass("disabled").append($('<span class="bx bx-info-circle disabled-tooltip" />').attr("title", t("attachments_actions.open_custom_client_only")));
} }
} }
async openAttachmentCommand() { async openAttachmentCommand() {
@ -141,7 +151,9 @@ export default class AttachmentActionsWidget extends BasicWidget {
} }
async copyAttachmentLinkToClipboardCommand() { async copyAttachmentLinkToClipboardCommand() {
this.parent.copyAttachmentLinkToClipboard(); if (this.parent && "copyAttachmentLinkToClipboard" in this.parent) {
(this.parent as AttachmentDetailWidget).copyAttachmentLinkToClipboard();
}
} }
async deleteAttachmentCommand() { async deleteAttachmentCommand() {
@ -158,7 +170,8 @@ export default class AttachmentActionsWidget extends BasicWidget {
return; return;
} }
const { note: newNote } = await server.post(`attachments/${this.attachmentId}/convert-to-note`);
const { note: newNote } = await server.post<ReturnType<typeof attachmentsApiRoute.convertAttachmentToNote>>(`attachments/${this.attachmentId}/convert-to-note`);
toastService.showMessage(t("attachments_actions.convert_success", { title: this.attachment.title })); toastService.showMessage(t("attachments_actions.convert_success", { title: this.attachment.title }));
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(newNote.noteId); await appContext.tabManager.getActiveContext().setNote(newNote.noteId);

View File

@ -4,6 +4,8 @@ import linkService from "../../services/link.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
import type { EventData } from "../../components/app_context.js";
const TPL = ` const TPL = `
<div class="attachment-detail note-detail-printable"> <div class="attachment-detail note-detail-printable">
@ -32,6 +34,9 @@ const TPL = `
</div>`; </div>`;
export default class AttachmentDetailTypeWidget extends TypeWidget { export default class AttachmentDetailTypeWidget extends TypeWidget {
$wrapper!: JQuery<HTMLElement>;
$linksWrapper!: JQuery<HTMLElement>;
static getType() { static getType() {
return "attachmentDetail"; return "attachmentDetail";
} }
@ -44,7 +49,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget {
super.doRender(); super.doRender();
} }
async doRefresh(note) { async doRefresh(note: Parameters<TypeWidget["doRefresh"]>[0]) {
this.$wrapper.empty(); this.$wrapper.empty();
this.children = []; this.children = [];
@ -69,7 +74,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget {
$helpButton $helpButton
); );
const attachment = await froca.getAttachment(this.attachmentId, true); const attachment = (this.attachmentId) ? await froca.getAttachment(this.attachmentId, true) : null;
if (!attachment) { if (!attachment) {
this.$wrapper.html("<strong>" + t("attachment_detail.attachment_deleted") + "</strong>"); this.$wrapper.html("<strong>" + t("attachment_detail.attachment_deleted") + "</strong>");
@ -82,7 +87,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget {
this.$wrapper.append(attachmentDetailWidget.render()); this.$wrapper.append(attachmentDetailWidget.render());
} }
async entitiesReloadedEvent({ loadResults }) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
const attachmentRow = loadResults.getAttachmentRows().find((att) => att.attachmentId === this.attachmentId); const attachmentRow = loadResults.getAttachmentRows().find((att) => att.attachmentId === this.attachmentId);
if (attachmentRow?.isDeleted) { if (attachmentRow?.isDeleted) {
@ -91,6 +96,6 @@ export default class AttachmentDetailTypeWidget extends TypeWidget {
} }
get attachmentId() { get attachmentId() {
return this.noteContext.viewScope.attachmentId; return this?.noteContext?.viewScope?.attachmentId;
} }
} }

View File

@ -3,6 +3,7 @@ import AttachmentDetailWidget from "../attachment_detail.js";
import linkService from "../../services/link.js"; import linkService from "../../services/link.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js";
const TPL = ` const TPL = `
<div class="attachment-list note-detail-printable"> <div class="attachment-list note-detail-printable">
@ -27,6 +28,10 @@ const TPL = `
</div>`; </div>`;
export default class AttachmentListTypeWidget extends TypeWidget { export default class AttachmentListTypeWidget extends TypeWidget {
$list!: JQuery<HTMLElement>;
$linksWrapper!: JQuery<HTMLElement>;
renderedAttachmentIds!: Set<string>;
static getType() { static getType() {
return "attachmentList"; return "attachmentList";
} }
@ -39,7 +44,10 @@ export default class AttachmentListTypeWidget extends TypeWidget {
super.doRender(); super.doRender();
} }
async doRefresh(note) { async doRefresh(note: Parameters<TypeWidget["doRefresh"]>[0]) {
// TriliumNextTODO: do we need to handle an undefined/null note?
if (!note) return false;
const $helpButton = $(` const $helpButton = $(`
<button class="attachment-help-button icon-action bx bx-help-circle" <button class="attachment-help-button icon-action bx bx-help-circle"
type="button" data-help-page="attachments.html" type="button" data-help-page="attachments.html"
@ -56,7 +64,11 @@ export default class AttachmentListTypeWidget extends TypeWidget {
$(`<div class="attachment-actions-toolbar">`).append( $(`<div class="attachment-actions-toolbar">`).append(
$('<button class="btn btn-sm">') $('<button class="btn btn-sm">')
.text(t("attachment_list.upload_attachments")) .text(t("attachment_list.upload_attachments"))
.on("click", () => this.triggerCommand("showUploadAttachmentsDialog", { noteId: this.noteId })), .on("click", () => {
if (this.noteId) {
this.triggerCommand("showUploadAttachmentsDialog", { noteId: this.noteId })
}
}),
$helpButton $helpButton
) )
); );
@ -83,9 +95,9 @@ export default class AttachmentListTypeWidget extends TypeWidget {
} }
} }
async entitiesReloadedEvent({ loadResults }) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// updates and deletions are handled by the detail, for new attachments the whole list has to be refreshed // updates and deletions are handled by the detail, for new attachments the whole list has to be refreshed
const attachmentsAdded = loadResults.getAttachmentRows().some((att) => !this.renderedAttachmentIds.has(att.attachmentId)); const attachmentsAdded = loadResults.getAttachmentRows().some((att) => att.attachmentId && !this.renderedAttachmentIds.has(att.attachmentId));
if (attachmentsAdded) { if (attachmentsAdded) {
this.refresh(); this.refresh();