feat(react/dialogs): port upload attachments

This commit is contained in:
Elian Doran 2025-08-05 23:03:38 +03:00
parent f196a78728
commit 2a40d6bb7e
No known key found for this signature in database
12 changed files with 103 additions and 129 deletions

View File

@ -310,7 +310,7 @@
"upload_attachments_to_note": "上传附件到笔记",
"close": "关闭",
"choose_files": "选择文件",
"files_will_be_uploaded": "文件将作为附件上传到",
"files_will_be_uploaded": "文件将作为附件上传到 {{noteTitle}}",
"options": "选项",
"shrink_images": "缩小图片",
"upload": "上传",

View File

@ -307,7 +307,7 @@
"upload_attachments_to_note": "Lade Anhänge zur Notiz hoch",
"close": "Schließen",
"choose_files": "Wähle Dateien aus",
"files_will_be_uploaded": "Dateien werden als Anhänge in hochgeladen",
"files_will_be_uploaded": "Dateien werden als Anhänge in hochgeladen {{noteTitle}}",
"options": "Optionen",
"shrink_images": "Bilder verkleinern",
"upload": "Hochladen",

View File

@ -316,7 +316,7 @@
"upload_attachments_to_note": "Upload attachments to note",
"close": "Close",
"choose_files": "Choose files",
"files_will_be_uploaded": "Files will be uploaded as attachments into",
"files_will_be_uploaded": "Files will be uploaded as attachments into {{noteTitle}}",
"options": "Options",
"shrink_images": "Shrink images",
"upload": "Upload",

View File

@ -312,7 +312,7 @@
"upload_attachments_to_note": "Cargar archivos adjuntos a nota",
"close": "Cerrar",
"choose_files": "Elija los archivos",
"files_will_be_uploaded": "Los archivos se cargarán como archivos adjuntos en",
"files_will_be_uploaded": "Los archivos se cargarán como archivos adjuntos en {{noteTitle}}",
"options": "Opciones",
"shrink_images": "Reducir imágenes",
"upload": "Subir",

View File

@ -307,7 +307,7 @@
"upload_attachments_to_note": "Téléverser des pièces jointes à la note",
"close": "Fermer",
"choose_files": "Choisir des fichiers",
"files_will_be_uploaded": "Les fichiers seront téléversés sous forme de pièces jointes dans",
"files_will_be_uploaded": "Les fichiers seront téléversés sous forme de pièces jointes dans {{noteTitle}}",
"options": "Options",
"shrink_images": "Réduire les images",
"upload": "Téléverser",

View File

@ -1309,7 +1309,7 @@
},
"upload_attachments": {
"choose_files": "Selectați fișierele",
"files_will_be_uploaded": "Fișierele vor fi încărcate ca atașamente în",
"files_will_be_uploaded": "Fișierele vor fi încărcate ca atașamente în {{noteTitle}}",
"options": "Opțuni",
"shrink_images": "Micșorează imaginile",
"tooltip": "Dacă această opțiune este bifată, Trilium va încerca micșorarea imaginilor încărcate prin scalarea și optimizarea lor, aspect ce va putea afecta calitatea imaginilor. Dacă nu este bifată, imaginile vor fi încărcate fără nicio schimbare.",

View File

@ -315,7 +315,7 @@
"upload_attachments_to_note": "Otpremite priloge uz belešku",
"close": "Zatvori",
"choose_files": "Izaberite datoteke",
"files_will_be_uploaded": "Datoteke će biti otpremljene kao prilozi u",
"files_will_be_uploaded": "Datoteke će biti otpremljene kao prilozi u {{noteTitle}}",
"options": "Opcije",
"shrink_images": "Smanji slike",
"upload": "Otpremi",

View File

@ -281,7 +281,7 @@
"upload_attachments": {
"upload_attachments_to_note": "上傳附件到筆記",
"choose_files": "選擇文件",
"files_will_be_uploaded": "文件將作為附件上傳到",
"files_will_be_uploaded": "文件將作為附件上傳到 {{noteTitle}}",
"options": "選項",
"shrink_images": "縮小圖片",
"upload": "上傳",

View File

@ -1,120 +0,0 @@
import { t } from "../../services/i18n.js";
import { escapeQuotes } 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";
import { Modal, Tooltip } from "bootstrap";
import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/`
<div class="upload-attachments-dialog modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${t("upload_attachments.upload_attachments_to_note")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("upload_attachments.close")}"></button>
</div>
<form class="upload-attachment-form">
<div class="modal-body">
<div class="form-group">
<label for="upload-attachment-file-upload-input"><strong>${t("upload_attachments.choose_files")}</strong></label>
<label class="tn-file-input tn-input-field">
<input type="file" class="upload-attachment-file-upload-input form-control-file" multiple />
</label>
<p>${t("upload_attachments.files_will_be_uploaded")} <strong class="upload-attachment-note-title"></strong>.</p>
</div>
<div class="form-group">
<strong>${t("upload_attachments.options")}:</strong>
<div class="checkbox">
<label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("upload_attachments.tooltip"))}">
<input class="shrink-images-checkbox form-check-input" value="1" type="checkbox" checked> <span>${t("upload_attachments.shrink_images")}</span>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="upload-attachment-button btn btn-primary">${t("upload_attachments.upload")}</button>
</div>
</form>
</div>
</div>
</div>`;
export default class UploadAttachmentsDialog extends BasicWidget {
private parentNoteId: string | null;
private modal!: bootstrap.Modal;
private $form!: JQuery<HTMLElement>;
private $noteTitle!: JQuery<HTMLElement>;
private $fileUploadInput!: JQuery<HTMLInputElement>;
private $uploadButton!: JQuery<HTMLElement>;
private $shrinkImagesCheckbox!: JQuery<HTMLElement>;
constructor() {
super();
this.parentNoteId = null;
}
doRender() {
this.$widget = $(TPL);
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
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");
if (this.parentNoteId) {
this.uploadAttachments(this.parentNoteId);
}
return false;
});
this.$fileUploadInput.on("change", () => {
if (this.$fileUploadInput.val()) {
this.$uploadButton.removeAttr("disabled");
} else {
this.$uploadButton.attr("disabled", "disabled");
}
});
Tooltip.getOrCreateInstance(this.$widget.find('[data-bs-toggle="tooltip"]')[0], {
html: true
});
}
async showUploadAttachmentsDialogEvent({ noteId }: EventData<"showUploadAttachmentsDialog">) {
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));
openDialog(this.$widget);
}
async uploadAttachments(parentNoteId: string) {
const files = Array.from(this.$fileUploadInput[0].files ?? []); // shallow copy since we're resetting the upload button below
function boolToString($el: JQuery<HTMLElement>): "true" | "false" {
return ($el.is(":checked") ? "true" : "false");
}
const options = {
shrinkImages: boolToString(this.$shrinkImagesCheckbox)
};
this.modal.hide();
await importService.uploadFiles("attachments", parentNoteId, files, options);
}
}

View File

@ -0,0 +1,79 @@
import { useEffect, useState } from "preact/compat";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import FormCheckbox from "../react/FormCheckbox";
import FormFileUpload from "../react/FormFileUpload";
import FormGroup from "../react/FormGroup";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import options from "../../services/options";
import importService from "../../services/import.js";
import { EventData } from "../../components/app_context";
import tree from "../../services/tree";
interface UploadAttachmentsDialogProps {
parentNoteId?: string;
}
function UploadAttachmentsDialogComponent({ parentNoteId }: UploadAttachmentsDialogProps) {
const [ files, setFiles ] = useState<FileList | null>(null);
const [ shrinkImages, setShrinkImages ] = useState(options.is("compressImages"));
const [ isUploading, setIsUploading ] = useState(false);
const [ description, setDescription ] = useState<string | undefined>(undefined);
if (parentNoteId) {
useEffect(() => {
tree.getNoteTitle(parentNoteId).then((noteTitle) =>
setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle })));
}, [parentNoteId]);
}
return (parentNoteId &&
<Modal
className="upload-attachments-dialog"
size="lg"
title={t("upload_attachments.upload_attachments_to_note")}
footer={<Button text={t("upload_attachments.upload")} primary disabled={!files || isUploading} />}
onSubmit={async () => {
if (!files) {
return;
}
setIsUploading(true);
const filesCopy = Array.from(files);
await importService.uploadFiles("attachments", parentNoteId, filesCopy, { shrinkImages });
setIsUploading(false);
closeActiveDialog();
}}
>
<FormGroup label={t("upload_attachments.choose_files")} description={description}>
<FormFileUpload onChange={setFiles} multiple />
</FormGroup>
<FormGroup label={t("upload_attachments.options")}>
<FormCheckbox
name="shrink-images"
hint={t("upload_attachments.tooltip")} label={t("upload_attachments.shrink_images")}
currentValue={shrinkImages} onChange={setShrinkImages}
/>
</FormGroup>
</Modal>
);
}
export default class UploadAttachmentsDialog extends ReactBasicWidget {
private props: UploadAttachmentsDialogProps = {};
get component() {
return <UploadAttachmentsDialogComponent {...this.props} />;
}
showUploadAttachmentsDialogEvent({ noteId }: EventData<"showUploadAttachmentsDialog">) {
this.props = { parentNoteId: noteId };
this.doRender();
openDialog(this.$widget);
}
}

View File

@ -11,9 +11,10 @@ interface ButtonProps {
/** Called when the button is clicked. If not set, the button will submit the form (if any). */
onClick?: () => void;
primary?: boolean;
disabled?: boolean;
}
export default function Button({ buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary }: ButtonProps) {
export default function Button({ buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled }: ButtonProps) {
const classes: string[] = ["btn"];
if (primary) {
classes.push("btn-primary");
@ -33,6 +34,7 @@ export default function Button({ buttonRef: _buttonRef, className, text, onClick
type={onClick ? "button" : "submit"}
onClick={onClick}
ref={buttonRef}
disabled={disabled}
>
{icon && <span className={`bx ${icon}`}></span>}
{text} {keyboardShortcut && (

View File

@ -0,0 +1,13 @@
interface FormFileUploadProps {
onChange: (files: FileList | null) => void;
multiple?: boolean;
}
export default function FormFileUpload({ onChange, multiple }: FormFileUploadProps) {
return (
<label class="tn-file-input tn-input-field">
<input type="file" class="form-control-file" multiple={multiple}
onChange={e => onChange((e.target as HTMLInputElement).files)} />
</label>
)
}