feat(react/ribbon): port file notes

This commit is contained in:
Elian Doran 2025-08-22 20:17:00 +03:00
parent cc05572a35
commit 978d829150
No known key found for this signature in database
7 changed files with 141 additions and 162 deletions

View File

@ -20,5 +20,10 @@
"scope": "typescript",
"prefix": "jqf",
"body": ["private $${1:name}!: JQuery<HTMLElement>;"]
},
"region": {
"scope": "css",
"prefix": "region",
"body": ["/* #region ${1:name} */\n$0\n/* #endregion */"]
}
}

View File

@ -35,7 +35,7 @@ function download(url: string) {
}
}
function downloadFileNote(noteId: string) {
export function downloadFileNote(noteId: string) {
const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
download(url);
@ -163,7 +163,7 @@ async function openExternally(type: string, entityId: string, mime: string) {
}
}
const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
export const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime);
function getHost() {

View File

@ -1,12 +1,22 @@
import { Ref } from "preact";
interface FormFileUploadProps {
name?: string;
onChange: (files: FileList | null) => void;
multiple?: boolean;
hidden?: boolean;
inputRef?: Ref<HTMLInputElement>;
}
export default function FormFileUpload({ onChange, multiple }: FormFileUploadProps) {
export default function FormFileUpload({ inputRef, name, onChange, multiple, hidden }: FormFileUploadProps) {
return (
<label class="tn-file-input tn-input-field">
<input type="file" class="form-control-file" multiple={multiple}
<label class="tn-file-input tn-input-field" style={hidden ? { display: "none" } : undefined}>
<input
ref={inputRef}
name={name}
type="file"
class="form-control-file"
multiple={multiple}
onChange={e => onChange((e.target as HTMLInputElement).files)} />
</label>
)

View File

@ -0,0 +1,97 @@
import { useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import { formatSize } from "../../services/utils";
import FormFileUpload from "../react/FormFileUpload";
import { useNoteLabel, useTriliumEventBeta } from "../react/hooks";
import { TabContext } from "./ribbon-interface";
import FBlob from "../../entities/fblob";
import Button from "../react/Button";
import protected_session_holder from "../../services/protected_session_holder";
import { downloadFileNote, openNoteExternally } from "../../services/open";
import toast from "../../services/toast";
import server from "../../services/server";
export default function FilePropertiesTab({ note }: TabContext) {
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
const [ blob, setBlob ] = useState<FBlob | null>();
const canAccessProtectedNote = !note?.isProtected || protected_session_holder.isProtectedSessionAvailable();
const inputRef = useRef<HTMLInputElement>(null);
function refresh() {
note?.getBlob().then(setBlob);
}
useEffect(refresh, [ note?.noteId ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.hasRevisionForNote(note.noteId)) {
refresh();
}
});
return (
<div className="file-properties-widget">
{note && (
<table class="file-table">
<tr>
<th class="text-nowrap">{t("file_properties.note_id")}:</th>
<td class="file-note-id">{note.noteId}</td>
<th class="text-nowrap">{t("file_properties.original_file_name")}:</th>
<td class="file-filename">{originalFileName ?? "?"}</td>
</tr>
<tr>
<th class="text-nowrap">{t("file_properties.file_type")}:</th>
<td class="file-filetype">{note.mime}</td>
<th class="text-nowrap">{t("file_properties.file_size")}:</th>
<td class="file-filesize">{formatSize(blob?.contentLength ?? 0)}</td>
</tr>
<tr>
<td colSpan={4}>
<div class="file-buttons">
<Button
icon="bx bx-download"
text={t("file_properties.download")}
primary
disabled={!canAccessProtectedNote}
onClick={() => downloadFileNote(note.noteId)}
/>
<Button
icon="bx bx-link-external"
text={t("file_properties.open")}
disabled={note.isProtected}
onClick={() => openNoteExternally(note.noteId, note.mime)}
/>
<Button
icon="bx bx-folder-open"
text={t("file_properties.upload_new_revision")}
disabled={!canAccessProtectedNote}
onClick={() => inputRef.current?.click()}
/>
<FormFileUpload
inputRef={inputRef}
hidden
onChange={(fileToUpload) => {
if (!fileToUpload) {
return;
}
server.upload(`notes/${note.noteId}/file`, fileToUpload[0]).then((result) => {
if (result.uploaded) {
toast.showMessage(t("file_properties.upload_success"));
} else {
toast.showError(t("file_properties.upload_failed"));
}
});
}}
/>
</div>
</td>
</tr>
</table>
)}
</div>
);
}

View File

@ -15,6 +15,7 @@ import EditedNotesTab from "./EditedNotesTab";
import NotePropertiesTab from "./NotePropertiesTab";
import NoteInfoTab from "./NoteInfoTab";
import SimilarNotesTab from "./SimilarNotesTab";
import FilePropertiesTab from "./FilePropertiesTab";
interface TitleContext {
note: FNote | null | undefined;
@ -77,9 +78,12 @@ const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>([
activate: true
},
{
// FilePropertiesWidget
title: t("file_properties.title"),
icon: "bx bx-file"
icon: "bx bx-file",
content: FilePropertiesTab,
show: ({ note }) => note?.type === "file",
toggleCommand: "toggleRibbonTabFileProperties",
activate: true
},
{
// ImagePropertiesWidget

View File

@ -203,4 +203,22 @@
white-space: nowrap;
overflow: hidden;
}
/* #endregion */
/* #region File Properties */
.file-table {
width: 100%;
margin-top: 10px;
}
.file-table th, .file-table td {
padding: 5px;
overflow-wrap: anywhere;
}
.file-buttons {
padding: 10px;
display: flex;
justify-content: space-evenly;
}
/* #endregion */

View File

@ -1,155 +0,0 @@
import server from "../../services/server.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import toastService from "../../services/toast.js";
import openService from "../../services/open.js";
import utils from "../../services/utils.js";
import protectedSessionHolder from "../../services/protected_session_holder.js";
import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
const TPL = /*html*/`
<div class="file-properties-widget">
<style>
.file-table {
width: 100%;
margin-top: 10px;
}
.file-table th, .file-table td {
padding: 5px;
overflow-wrap: anywhere;
}
.file-buttons {
padding: 10px;
display: flex;
justify-content: space-evenly;
}
</style>
<table class="file-table">
<tr>
<th class="text-nowrap">${t("file_properties.note_id")}:</th>
<td class="file-note-id"></td>
<th class="text-nowrap">${t("file_properties.original_file_name")}:</th>
<td class="file-filename"></td>
</tr>
<tr>
<th class="text-nowrap">${t("file_properties.file_type")}:</th>
<td class="file-filetype"></td>
<th class="text-nowrap">${t("file_properties.file_size")}:</th>
<td class="file-filesize"></td>
</tr>
<tr>
<td colspan="4">
<div class="file-buttons">
<button class="file-download btn btn-sm btn-primary" type="button">
<span class="bx bx-download"></span>
${t("file_properties.download")}
</button>
<button class="file-open btn btn-sm btn-primary" type="button">
<span class="bx bx-link-external"></span>
${t("file_properties.open")}
</button>
<button class="file-upload-new-revision btn btn-sm btn-primary">
<span class="bx bx-folder-open"></span>
${t("file_properties.upload_new_revision")}
</button>
<input type="file" class="file-upload-new-revision-input" style="display: none">
</div>
</td>
</tr>
</table>
</div>`;
export default class FilePropertiesWidget extends NoteContextAwareWidget {
private $fileNoteId!: JQuery<HTMLElement>;
private $fileName!: JQuery<HTMLElement>;
private $fileType!: JQuery<HTMLElement>;
private $fileSize!: JQuery<HTMLElement>;
private $downloadButton!: JQuery<HTMLElement>;
private $openButton!: JQuery<HTMLElement>;
private $uploadNewRevisionButton!: JQuery<HTMLElement>;
private $uploadNewRevisionInput!: JQuery<HTMLFormElement>;
get name() {
return "fileProperties";
}
get toggleCommand() {
return "toggleRibbonTabFileProperties";
}
isEnabled() {
return this.note && this.note.type === "file";
}
getTitle() {
return {
show: this.isEnabled(),
activate: true,
};
}
doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$fileNoteId = this.$widget.find(".file-note-id");
this.$fileName = this.$widget.find(".file-filename");
this.$fileType = this.$widget.find(".file-filetype");
this.$fileSize = this.$widget.find(".file-filesize");
this.$downloadButton = this.$widget.find(".file-download");
this.$openButton = this.$widget.find(".file-open");
this.$uploadNewRevisionButton = this.$widget.find(".file-upload-new-revision");
this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input");
this.$downloadButton.on("click", () => this.noteId && openService.downloadFileNote(this.noteId));
this.$openButton.on("click", () => this.noteId && this.note && openService.openNoteExternally(this.noteId, this.note.mime));
this.$uploadNewRevisionButton.on("click", () => {
this.$uploadNewRevisionInput.trigger("click");
});
this.$uploadNewRevisionInput.on("change", async () => {
const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
this.$uploadNewRevisionInput.val("");
const result = await server.upload(`notes/${this.noteId}/file`, fileToUpload);
if (result.uploaded) {
toastService.showMessage(t("file_properties.upload_success"));
this.refresh();
} else {
toastService.showError(t("file_properties.upload_failed"));
}
});
}
async refreshWithNote(note: FNote) {
this.$widget.show();
if (!this.note) {
return;
}
this.$fileNoteId.text(note.noteId);
this.$fileName.text(note.getLabelValue("originalFileName") || "?");
this.$fileType.text(note.mime);
const blob = await this.note.getBlob();
this.$fileSize.text(utils.formatSize(blob?.contentLength ?? 0));
// open doesn't work for protected notes since it works through a browser which isn't in protected session
this.$openButton.toggle(!note.isProtected);
this.$downloadButton.toggle(!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable());
this.$uploadNewRevisionButton.toggle(!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable());
}
}