feat(react/ribbon): port note info tab

This commit is contained in:
Elian Doran 2025-08-22 18:23:54 +03:00
parent 70728c274e
commit 77551b1fed
No known key found for this signature in database
10 changed files with 148 additions and 181 deletions

View File

@ -185,7 +185,11 @@ export function escapeQuotes(value: string) {
return value.replaceAll('"', """);
}
function formatSize(size: number) {
export function formatSize(size: number | null | undefined) {
if (size === null || size === undefined) {
return "";
}
size = Math.max(Math.round(size / 1024), 1);
if (size < 1024) {

View File

@ -3,7 +3,11 @@ type DateTimeStyle = "full" | "long" | "medium" | "short" | "none" | undefined;
/**
* Formats the given date and time to a string based on the current locale.
*/
export function formatDateTime(date: string | Date | number, dateStyle: DateTimeStyle = "medium", timeStyle: DateTimeStyle = "medium") {
export function formatDateTime(date: string | Date | number | null | undefined, dateStyle: DateTimeStyle = "medium", timeStyle: DateTimeStyle = "medium") {
if (!date) {
return "";
}
const locale = navigator.language;
let parsedDate;

View File

@ -0,0 +1,3 @@
export default function LoadingSpinner() {
return <span className="bx bx-loader bx-spin" />
}

View File

@ -0,0 +1,84 @@
import { useEffect, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import { TabContext } from "./ribbon-interface";
import { MetadataResponse, NoteSizeResponse, SubtreeSizeResponse } from "@triliumnext/commons";
import server from "../../services/server";
import Button from "../react/Button";
import { formatDateTime } from "../../utils/formatters";
import { formatSize } from "../../services/utils";
import LoadingSpinner from "../react/LoadingSpinner";
export default function NoteInfoTab({ note }: TabContext) {
const [ metadata, setMetadata ] = useState<MetadataResponse>();
const [ isLoading, setIsLoading ] = useState(false);
const [ noteSizeResponse, setNoteSizeResponse ] = useState<NoteSizeResponse>();
const [ subtreeSizeResponse, setSubtreeSizeResponse ] = useState<SubtreeSizeResponse>();
useEffect(() => {
if (note) {
server.get<MetadataResponse>(`notes/${note?.noteId}/metadata`).then(setMetadata);
}
setNoteSizeResponse(undefined);
setSubtreeSizeResponse(undefined);
setIsLoading(false);
}, [ note?.noteId ]);
console.log("Got ", noteSizeResponse, subtreeSizeResponse);
return (
<div className="note-info-widget">
{note && (
<table className="note-info-widget-table">
<tr>
<th>{t("note_info_widget.note_id")}:</th>
<td>{note.noteId}</td>
<th>{t("note_info_widget.created")}:</th>
<td>{formatDateTime(metadata?.dateCreated)}</td>
<th>{t("note_info_widget.modified")}:</th>
<td>{formatDateTime(metadata?.dateModified)}</td>
</tr>
<tr>
<th>{t("note_info_widget.type")}:</th>
<td>
<span class="note-info-type">{note.type}</span>{' '}
{ note.mime && <span class="note-info-mime">({note.mime})</span> }
</td>
<th title={t("note_info_widget.note_size_info")}>{t("note_info_widget.note_size")}:</th>
<td colSpan={3}>
{!isLoading && !noteSizeResponse && !subtreeSizeResponse && (
<Button
className="calculate-button"
style={{ padding: "0px 10px 0px 10px" }}
icon="bx bx-calculator"
text={t("note_info_widget.calculate")}
onClick={() => {
setIsLoading(true);
setTimeout(async () => {
await Promise.allSettled([
server.get<NoteSizeResponse>(`stats/note-size/${note.noteId}`).then(setNoteSizeResponse),
server.get<SubtreeSizeResponse>(`stats/subtree-size/${note.noteId}`).then(setSubtreeSizeResponse)
]);
setIsLoading(false);
}, 0);
}}
/>
)}
<span className="note-sizes-wrapper">
<span class="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
{" "}
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&
<span class="subtree-size">{t("note_info_widget.subtree_size", { size: formatSize(subtreeSizeResponse.subTreeSize), count: subtreeSizeResponse.subTreeNoteCount })}</span>
}
{isLoading && <LoadingSpinner />}
</span>
</td>
</tr>
</table>
)}
</div>
)
}

View File

@ -13,6 +13,7 @@ import FNote from "../../entities/fnote";
import ScriptTab from "./ScriptTab";
import EditedNotesTab from "./EditedNotesTab";
import NotePropertiesTab from "./NotePropertiesTab";
import NoteInfoTab from "./NoteInfoTab";
interface TitleContext {
note: FNote | null | undefined;
@ -118,9 +119,11 @@ const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>([
icon: "bx bx-bar-chart"
},
{
// NoteInfoWidget
title: t("note_info_widget.title"),
icon: "bx bx-info-circle"
icon: "bx bx-info-circle",
show: ({ note }) => !!note,
content: NoteInfoTab,
toggleCommand: "toggleRibbonTabNoteInfo"
}
]);
@ -128,7 +131,7 @@ export default function Ribbon() {
const { note } = useNoteContext();
const titleContext: TitleContext = { note };
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
const filteredTabs = useMemo(() => TAB_CONFIGURATION.filter(tab => tab.show?.(titleContext)), [ titleContext, note ])
const filteredTabs = useMemo(() => TAB_CONFIGURATION.filter(tab => tab.show?.(titleContext)), [ titleContext, note ]);
return (
<div class="ribbon-container" style={{ contain: "none" }}>

View File

@ -158,4 +158,28 @@
.execute-description {
margin-bottom: 10px;
}
/* #endregion */
/* #region Note info */
.note-info-widget {
padding: 12px;
}
.note-info-widget-table {
max-width: 100%;
display: block;
overflow-x: auto;
white-space: nowrap;
}
.note-info-widget-table td, .note-info-widget-table th {
padding: 5px;
}
.note-info-mime {
max-width: 13em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* #endregion */

View File

@ -1,172 +0,0 @@
import { formatDateTime } from "../../utils/formatters.js";
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import server from "../../services/server.js";
import utils from "../../services/utils.js";
import type { EventData } from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
const TPL = /*html*/`
<div class="note-info-widget">
<style>
.note-info-widget {
padding: 12px;
}
.note-info-widget-table {
max-width: 100%;
display: block;
overflow-x: auto;
white-space: nowrap;
}
.note-info-widget-table td, .note-info-widget-table th {
padding: 5px;
}
.note-info-mime {
max-width: 13em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
<table class="note-info-widget-table">
<tr>
<th>${t("note_info_widget.note_id")}:</th>
<td class="note-info-note-id"></td>
<th>${t("note_info_widget.created")}:</th>
<td class="note-info-date-created"></td>
<th>${t("note_info_widget.modified")}:</th>
<td class="note-info-date-modified"></td>
</tr>
<tr>
<th>${t("note_info_widget.type")}:</th>
<td>
<span class="note-info-type"></span>
<span class="note-info-mime"></span>
</td>
<th title="${t("note_info_widget.note_size_info")}">${t("note_info_widget.note_size")}:</th>
<td colspan="3">
<button class="btn btn-sm calculate-button" style="padding: 0px 10px 0px 10px;">
<span class="bx bx-calculator"></span> ${t("note_info_widget.calculate")}
</button>
<span class="note-sizes-wrapper">
<span class="note-size"></span>
<span class="subtree-size"></span>
</span>
</td>
</tr>
</table>
</div>
`;
// TODO: Deduplicate with server
interface NoteSizeResponse {
noteSize: number;
}
interface SubtreeSizeResponse {
subTreeNoteCount: number;
subTreeSize: number;
}
interface MetadataResponse {
dateCreated: number;
dateModified: number;
}
export default class NoteInfoWidget extends NoteContextAwareWidget {
private $noteId!: JQuery<HTMLElement>;
private $dateCreated!: JQuery<HTMLElement>;
private $dateModified!: JQuery<HTMLElement>;
private $type!: JQuery<HTMLElement>;
private $mime!: JQuery<HTMLElement>;
private $noteSizesWrapper!: JQuery<HTMLElement>;
private $noteSize!: JQuery<HTMLElement>;
private $subTreeSize!: JQuery<HTMLElement>;
private $calculateButton!: JQuery<HTMLElement>;
get name() {
return "noteInfo";
}
get toggleCommand() {
return "toggleRibbonTabNoteInfo";
}
isEnabled() {
return !!this.note;
}
getTitle() {
return {
show: this.isEnabled(),
};
}
doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$noteId = this.$widget.find(".note-info-note-id");
this.$dateCreated = this.$widget.find(".note-info-date-created");
this.$dateModified = this.$widget.find(".note-info-date-modified");
this.$type = this.$widget.find(".note-info-type");
this.$mime = this.$widget.find(".note-info-mime");
this.$noteSizesWrapper = this.$widget.find(".note-sizes-wrapper");
this.$noteSize = this.$widget.find(".note-size");
this.$subTreeSize = this.$widget.find(".subtree-size");
this.$calculateButton = this.$widget.find(".calculate-button");
this.$calculateButton.on("click", async () => {
this.$noteSizesWrapper.show();
this.$calculateButton.hide();
this.$noteSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
this.$subTreeSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
const noteSizeResp = await server.get<NoteSizeResponse>(`stats/note-size/${this.noteId}`);
this.$noteSize.text(utils.formatSize(noteSizeResp.noteSize));
const subTreeResp = await server.get<SubtreeSizeResponse>(`stats/subtree-size/${this.noteId}`);
if (subTreeResp.subTreeNoteCount > 1) {
this.$subTreeSize.text(t("note_info_widget.subtree_size", { size: utils.formatSize(subTreeResp.subTreeSize), count: subTreeResp.subTreeNoteCount }));
} else {
this.$subTreeSize.text("");
}
});
}
async refreshWithNote(note: FNote) {
const metadata = await server.get<MetadataResponse>(`notes/${this.noteId}/metadata`);
this.$noteId.text(note.noteId);
this.$dateCreated.text(formatDateTime(metadata.dateCreated)).attr("title", metadata.dateCreated);
this.$dateModified.text(formatDateTime(metadata.dateModified)).attr("title", metadata.dateModified);
this.$type.text(note.type);
if (note.mime) {
this.$mime.text(`(${note.mime})`);
} else {
this.$mime.empty();
}
this.$calculateButton.show();
this.$noteSizesWrapper.hide();
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.noteId && (loadResults.isNoteReloaded(this.noteId) || loadResults.isNoteContentReloaded(this.noteId))) {
this.refresh();
}
}
}

View File

@ -12,7 +12,7 @@ import ValidationError from "../../errors/validation_error.js";
import blobService from "../../services/blob.js";
import type { Request } from "express";
import type BBranch from "../../becca/entities/bbranch.js";
import type { AttributeRow, DeleteNotesPreview } from "@triliumnext/commons";
import type { AttributeRow, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
/**
* @swagger
@ -101,7 +101,7 @@ function getNoteMetadata(req: Request) {
utcDateCreated: note.utcDateCreated,
dateModified: note.dateModified,
utcDateModified: note.utcDateModified
};
} satisfies MetadataResponse;
}
function createNote(req: Request) {

View File

@ -1,6 +1,7 @@
import sql from "../../services/sql.js";
import becca from "../../becca/becca.js";
import type { Request } from "express";
import { NoteSizeResponse, SubtreeSizeResponse } from "@triliumnext/commons";
function getNoteSize(req: Request) {
const { noteId } = req.params;
@ -22,7 +23,7 @@ function getNoteSize(req: Request) {
return {
noteSize
};
} satisfies NoteSizeResponse;
}
function getSubtreeSize(req: Request) {
@ -45,7 +46,7 @@ function getSubtreeSize(req: Request) {
return {
subTreeSize,
subTreeNoteCount: subTreeNoteIds.length
};
} satisfies SubtreeSizeResponse;
}
export default {

View File

@ -169,3 +169,19 @@ export type EditedNotesResponse = {
title?: string;
notePath?: string[] | null;
}[];
export interface MetadataResponse {
dateCreated: string | undefined;
utcDateCreated: string;
dateModified: string | undefined;
utcDateModified: string | undefined;
}
export interface NoteSizeResponse {
noteSize: number;
}
export interface SubtreeSizeResponse {
subTreeNoteCount: number;
subTreeSize: number;
}