mirror of
https://github.com/TriliumNext/Trilium.git
synced 2025-12-11 05:45:26 -06:00
chore(react/type_widget): convert attachment actions
This commit is contained in:
parent
376ef0c679
commit
f2b4f49be2
@ -76,23 +76,4 @@ export default class AttachmentDetailWidget extends BasicWidget {
|
|||||||
this.$wrapper.find(".attachment-content-wrapper").append($renderedContent);
|
this.$wrapper.find(".attachment-content-wrapper").append($renderedContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyAttachmentLinkToClipboard() {
|
|
||||||
if (this.attachment.role === "image") {
|
|
||||||
imageService.copyImageReferenceToClipboard(this.$wrapper.find(".attachment-content-wrapper"));
|
|
||||||
} else if (this.attachment.role === "file") {
|
|
||||||
const $link = await linkService.createLink(this.attachment.ownerId, {
|
|
||||||
referenceLink: true,
|
|
||||||
viewScope: {
|
|
||||||
viewMode: "attachments",
|
|
||||||
attachmentId: this.attachment.attachmentId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.copyHtmlToClipboard($link[0].outerHTML);
|
|
||||||
|
|
||||||
toastService.showMessage(t("attachment_detail_2.link_copied"));
|
|
||||||
} else {
|
|
||||||
throw new Error(t("attachment_detail_2.unrecognized_role", { role: this.attachment.role }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
import { t } from "../../services/i18n.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
|
||||||
import server from "../../services/server.js";
|
|
||||||
import dialogService from "../../services/dialog.js";
|
|
||||||
import toastService from "../../services/toast.js";
|
|
||||||
import ws from "../../services/ws.js";
|
|
||||||
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 FAttachment from "../../entities/fattachment.js";
|
|
||||||
import type AttachmentDetailWidget from "../attachment_detail.js";
|
|
||||||
import type { NoteRow } from "@triliumnext/commons";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="dropdown">
|
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
|
||||||
<li data-trigger-command="downloadAttachment" class="dropdown-item">
|
|
||||||
<span class="bx bx-download"></span> ${t("attachments_actions.download")}</li>
|
|
||||||
|
|
||||||
<li data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item"><span class="bx bx-link">
|
|
||||||
</span> ${t("attachments_actions.copy_link_to_clipboard")}</li>
|
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
|
|
||||||
<li data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item"><span class="bx bx-upload">
|
|
||||||
</span> ${t("attachments_actions.upload_new_revision")}</li>
|
|
||||||
|
|
||||||
<li data-trigger-command="renameAttachment" class="dropdown-item">
|
|
||||||
<span class="bx bx-rename"></span> ${t("attachments_actions.rename_attachment")}</li>
|
|
||||||
|
|
||||||
<li data-trigger-command="deleteAttachment" class="dropdown-item">
|
|
||||||
<span class="bx bx-trash destructive-action-icon"></span> ${t("attachments_actions.delete_attachment")}</li>
|
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
|
|
||||||
<li data-trigger-command="convertAttachmentIntoNote" class="dropdown-item"><span class="bx bx-note">
|
|
||||||
</span> ${t("attachments_actions.convert_attachment_into_note")}</li>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="file" class="attachment-upload-new-revision-input" style="display: none">
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
// TODO: Deduplicate
|
|
||||||
interface AttachmentResponse {
|
|
||||||
note: NoteRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AttachmentActionsWidget extends BasicWidget {
|
|
||||||
$uploadNewRevisionInput!: JQuery<HTMLInputElement>;
|
|
||||||
attachment: FAttachment;
|
|
||||||
isFullDetail: boolean;
|
|
||||||
dropdown!: Dropdown;
|
|
||||||
|
|
||||||
constructor(attachment: FAttachment, isFullDetail: boolean) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.attachment = attachment;
|
|
||||||
this.isFullDetail = isFullDetail;
|
|
||||||
}
|
|
||||||
|
|
||||||
get attachmentId() {
|
|
||||||
return this.attachment.attachmentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
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?.item(0); // copy to allow reset below
|
|
||||||
this.$uploadNewRevisionInput.val("");
|
|
||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadAttachmentCommand() {
|
|
||||||
await openService.downloadAttachment(this.attachmentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async uploadNewAttachmentRevisionCommand() {
|
|
||||||
this.$uploadNewRevisionInput.trigger("click");
|
|
||||||
}
|
|
||||||
|
|
||||||
async copyAttachmentLinkToClipboardCommand() {
|
|
||||||
if (this.parent && "copyAttachmentLinkToClipboard" in this.parent) {
|
|
||||||
(this.parent as AttachmentDetailWidget).copyAttachmentLinkToClipboard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteAttachmentCommand() {
|
|
||||||
if (!(await dialogService.confirm(t("attachments_actions.delete_confirm", { title: this.attachment.title })))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.remove(`attachments/${this.attachmentId}`);
|
|
||||||
toastService.showMessage(t("attachments_actions.delete_success", { title: this.attachment.title }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async convertAttachmentIntoNoteCommand() {
|
|
||||||
if (!(await dialogService.confirm(t("attachments_actions.convert_confirm", { title: this.attachment.title })))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { note: newNote } = await server.post<AttachmentResponse>(`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);
|
|
||||||
}
|
|
||||||
|
|
||||||
async renameAttachmentCommand() {
|
|
||||||
const attachmentTitle = await dialogService.prompt({
|
|
||||||
title: t("attachments_actions.rename_attachment"),
|
|
||||||
message: t("attachments_actions.enter_new_name"),
|
|
||||||
defaultValue: this.attachment.title
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!attachmentTitle?.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.put(`attachments/${this.attachmentId}/rename`, { title: attachmentTitle });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -18,15 +18,18 @@ export default function FormFileUpload({ inputRef, name, onChange, multiple, hid
|
|||||||
name={name}
|
name={name}
|
||||||
type="file"
|
type="file"
|
||||||
class="form-control-file"
|
class="form-control-file"
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
onChange={e => onChange((e.target as HTMLInputElement).files)} />
|
onChange={e => {
|
||||||
|
onChange((e.target as HTMLInputElement).files);
|
||||||
|
e.currentTarget.value = "";
|
||||||
|
}} />
|
||||||
</label>
|
</label>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combination of a button with a hidden file upload field.
|
* Combination of a button with a hidden file upload field.
|
||||||
*
|
*
|
||||||
* @param param the change listener for the file upload and the properties for the button.
|
* @param param the change listener for the file upload and the properties for the button.
|
||||||
*/
|
*/
|
||||||
export function FormFileUploadButton({ onChange, ...buttonProps }: Omit<ButtonProps, "onClick"> & Pick<FormFileUploadProps, "onChange">) {
|
export function FormFileUploadButton({ onChange, ...buttonProps }: Omit<ButtonProps, "onClick"> & Pick<FormFileUploadProps, "onChange">) {
|
||||||
@ -39,10 +42,10 @@ export function FormFileUploadButton({ onChange, ...buttonProps }: Omit<ButtonPr
|
|||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
/>
|
/>
|
||||||
<FormFileUpload
|
<FormFileUpload
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
hidden
|
hidden
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import "./Attachment.css";
|
|||||||
import NoteLink from "../react/NoteLink";
|
import NoteLink from "../react/NoteLink";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { ParentComponent } from "../react/react_utils";
|
import { ParentComponent, refToJQuerySelector } from "../react/react_utils";
|
||||||
import HelpButton from "../react/HelpButton";
|
import HelpButton from "../react/HelpButton";
|
||||||
import FAttachment from "../../entities/fattachment";
|
import FAttachment from "../../entities/fattachment";
|
||||||
import Alert from "../react/Alert";
|
import Alert from "../react/Alert";
|
||||||
@ -14,8 +14,17 @@ import { useTriliumEvent } from "../react/hooks";
|
|||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
import Dropdown from "../react/Dropdown";
|
import Dropdown from "../react/Dropdown";
|
||||||
import Icon from "../react/Icon";
|
import Icon from "../react/Icon";
|
||||||
import { FormListItem } from "../react/FormList";
|
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||||
import open from "../../services/open";
|
import open from "../../services/open";
|
||||||
|
import toast from "../../services/toast";
|
||||||
|
import link from "../../services/link";
|
||||||
|
import image from "../../services/image";
|
||||||
|
import FormFileUpload from "../react/FormFileUpload";
|
||||||
|
import server from "../../services/server";
|
||||||
|
import dialog from "../../services/dialog";
|
||||||
|
import ws from "../../services/ws";
|
||||||
|
import appContext from "../../components/app_context";
|
||||||
|
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the full list of attachments of a note and allows the user to interact with them.
|
* Displays the full list of attachments of a note and allows the user to interact with them.
|
||||||
@ -126,11 +135,32 @@ function AttachmentInfo({ attachment, isFullDetail }: { attachment: FAttachment,
|
|||||||
})
|
})
|
||||||
}, [ attachment ]);
|
}, [ attachment ]);
|
||||||
|
|
||||||
|
async function copyAttachmentLinkToClipboard() {
|
||||||
|
if (attachment.role === "image") {
|
||||||
|
const $contentWrapper = refToJQuerySelector(contentWrapper);
|
||||||
|
image.copyImageReferenceToClipboard($contentWrapper);
|
||||||
|
} else if (attachment.role === "file") {
|
||||||
|
const $link = await link.createLink(attachment.ownerId, {
|
||||||
|
referenceLink: true,
|
||||||
|
viewScope: {
|
||||||
|
viewMode: "attachments",
|
||||||
|
attachmentId: attachment.attachmentId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
utils.copyHtmlToClipboard($link[0].outerHTML);
|
||||||
|
|
||||||
|
toast.showMessage(t("attachment_detail_2.link_copied"));
|
||||||
|
} else {
|
||||||
|
throw new Error(t("attachment_detail_2.unrecognized_role", { role: attachment.role }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="attachment-detail-widget">
|
<div className="attachment-detail-widget">
|
||||||
<div className="attachment-detail-wrapper">
|
<div className="attachment-detail-wrapper">
|
||||||
<div className="attachment-title-line">
|
<div className="attachment-title-line">
|
||||||
<AttachmentActions attachment={attachment} />
|
<AttachmentActions attachment={attachment} copyAttachmentLinkToClipboard={copyAttachmentLinkToClipboard} />
|
||||||
<h4 className="attachment-title">
|
<h4 className="attachment-title">
|
||||||
{!isFullDetail ? (
|
{!isFullDetail ? (
|
||||||
<NoteLink
|
<NoteLink
|
||||||
@ -155,8 +185,9 @@ function AttachmentInfo({ attachment, isFullDetail }: { attachment: FAttachment,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AttachmentActions({ attachment }: { attachment: FAttachment }) {
|
function AttachmentActions({ attachment, copyAttachmentLinkToClipboard }: { attachment: FAttachment, copyAttachmentLinkToClipboard: () => void }) {
|
||||||
const isElectron = utils.isElectron();
|
const isElectron = utils.isElectron();
|
||||||
|
const fileUploadRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="attachment-actions-container">
|
<div className="attachment-actions-container">
|
||||||
@ -169,16 +200,84 @@ function AttachmentActions({ attachment }: { attachment: FAttachment }) {
|
|||||||
<FormListItem
|
<FormListItem
|
||||||
icon="bx bx-file-find"
|
icon="bx bx-file-find"
|
||||||
title={t("attachments_actions.open_externally_title")}
|
title={t("attachments_actions.open_externally_title")}
|
||||||
onClick={(e) => open.openAttachmentExternally(attachment.attachmentId, attachment.mime)}
|
onClick={() => open.openAttachmentExternally(attachment.attachmentId, attachment.mime)}
|
||||||
>{t("attachments_actions.open_externally")}</FormListItem>
|
>{t("attachments_actions.open_externally")}</FormListItem>
|
||||||
|
|
||||||
<FormListItem
|
<FormListItem
|
||||||
icon="bx bx-customize"
|
icon="bx bx-customize"
|
||||||
title={t("attachments_actions.open_custom_title")}
|
title={t("attachments_actions.open_custom_title")}
|
||||||
onClick={(e) => open.openAttachmentCustom(attachment.attachmentId, attachment.mime)}
|
onClick={() => open.openAttachmentCustom(attachment.attachmentId, attachment.mime)}
|
||||||
disabled={!isElectron}
|
disabled={!isElectron}
|
||||||
disabledTooltip={!isElectron ? t("attachments_actions.open_custom_client_only") : t("attachments_actions.open_externally_detail_page")}
|
disabledTooltip={!isElectron ? t("attachments_actions.open_custom_client_only") : t("attachments_actions.open_externally_detail_page")}
|
||||||
>{t("attachments_actions.open_custom")}</FormListItem>
|
>{t("attachments_actions.open_custom")}</FormListItem>
|
||||||
|
<FormListItem
|
||||||
|
icon="bx bx-download"
|
||||||
|
onClick={() => open.downloadAttachment(attachment.attachmentId)}
|
||||||
|
>{t("attachments_actions.download")}</FormListItem>
|
||||||
|
<FormListItem
|
||||||
|
icon="bx bx-link"
|
||||||
|
onClick={copyAttachmentLinkToClipboard}
|
||||||
|
>{t("attachments_actions.copy_link_to_clipboard")}</FormListItem>
|
||||||
|
<FormDropdownDivider />
|
||||||
|
|
||||||
|
<FormListItem
|
||||||
|
icon="bx bx-upload"
|
||||||
|
onClick={() => fileUploadRef.current?.click()}
|
||||||
|
>{t("attachments_actions.upload_new_revision")}</FormListItem>
|
||||||
|
<FormListItem
|
||||||
|
icon="bx bx-rename"
|
||||||
|
onClick={async () => {
|
||||||
|
const attachmentTitle = await dialog.prompt({
|
||||||
|
title: t("attachments_actions.rename_attachment"),
|
||||||
|
message: t("attachments_actions.enter_new_name"),
|
||||||
|
defaultValue: attachment.title
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!attachmentTitle?.trim()) return;
|
||||||
|
await server.put(`attachments/${attachment.attachmentId}/rename`, { title: attachmentTitle });
|
||||||
|
}}
|
||||||
|
>{t("attachments_actions.rename_attachment")}</FormListItem>
|
||||||
|
<FormListItem
|
||||||
|
icon="bx bx-trash destructive-action-icon"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!(await dialog.confirm(t("attachments_actions.delete_confirm", { title: attachment.title })))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.remove(`attachments/${attachment.attachmentId}`);
|
||||||
|
toast.showMessage(t("attachments_actions.delete_success", { title: attachment.title }));
|
||||||
|
}}
|
||||||
|
>{t("attachments_actions.delete_attachment")}</FormListItem>
|
||||||
|
<FormDropdownDivider />
|
||||||
|
|
||||||
|
<FormListItem
|
||||||
|
icon="bx bx-note"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!(await dialog.confirm(t("attachments_actions.convert_confirm", { title: attachment.title })))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { note: newNote } = await server.post<ConvertAttachmentToNoteResponse>(`attachments/${attachment.attachmentId}/convert-to-note`);
|
||||||
|
toast.showMessage(t("attachments_actions.convert_success", { title: attachment.title }));
|
||||||
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
|
await appContext.tabManager.getActiveContext()?.setNote(newNote.noteId);
|
||||||
|
}}
|
||||||
|
>{t("attachments_actions.convert_attachment_into_note")}</FormListItem>
|
||||||
|
|
||||||
|
<FormFileUpload
|
||||||
|
inputRef={fileUploadRef}
|
||||||
|
hidden
|
||||||
|
onChange={async files => {
|
||||||
|
const fileToUpload = files?.item(0);
|
||||||
|
if (fileToUpload) {
|
||||||
|
const result = await server.upload(`attachments/${attachment.attachmentId}/file`, fileToUpload);
|
||||||
|
if (result.uploaded) {
|
||||||
|
toast.showMessage(t("attachments_actions.upload_success"));
|
||||||
|
} else {
|
||||||
|
toast.showError(t("attachments_actions.upload_failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import blobService from "../../services/blob.js";
|
|||||||
import ValidationError from "../../errors/validation_error.js";
|
import ValidationError from "../../errors/validation_error.js";
|
||||||
import imageService from "../../services/image.js";
|
import imageService from "../../services/image.js";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
|
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
function getAttachmentBlob(req: Request) {
|
function getAttachmentBlob(req: Request) {
|
||||||
const preview = req.query.preview === "true";
|
const preview = req.query.preview === "true";
|
||||||
@ -103,7 +104,7 @@ function convertAttachmentToNote(req: Request) {
|
|||||||
const { attachmentId } = req.params;
|
const { attachmentId } = req.params;
|
||||||
|
|
||||||
const attachment = becca.getAttachmentOrThrow(attachmentId);
|
const attachment = becca.getAttachmentOrThrow(attachmentId);
|
||||||
return attachment.convertToNote();
|
return attachment.convertToNote() satisfies ConvertAttachmentToNoteResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@ -207,6 +207,11 @@ export interface ConvertToAttachmentResponse {
|
|||||||
attachment: AttachmentRow;
|
attachment: AttachmentRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConvertAttachmentToNoteResponse {
|
||||||
|
note: NoteRow;
|
||||||
|
branch: BranchRow;
|
||||||
|
}
|
||||||
|
|
||||||
export type SaveSqlConsoleResponse = CloneResponse;
|
export type SaveSqlConsoleResponse = CloneResponse;
|
||||||
|
|
||||||
export interface BacklinkCountResponse {
|
export interface BacklinkCountResponse {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user