mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	feat(react/ribbon): port file notes
This commit is contained in:
		
							
								
								
									
										5
									
								
								.vscode/snippets.code-snippets
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.vscode/snippets.code-snippets
									
									
									
									
										vendored
									
									
								
							| @@ -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 */"] | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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> | ||||
|     ) | ||||
|   | ||||
							
								
								
									
										97
									
								
								apps/client/src/widgets/ribbon/FilePropertiesTab.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								apps/client/src/widgets/ribbon/FilePropertiesTab.tsx
									
									
									
									
									
										Normal 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> | ||||
|     ); | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -204,3 +204,21 @@ | ||||
|     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 */ | ||||
| @@ -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()); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user