mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 07:08:55 +02:00
feat(react/ribbon): port note info tab
This commit is contained in:
parent
70728c274e
commit
77551b1fed
@ -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) {
|
||||
|
@ -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;
|
||||
|
3
apps/client/src/widgets/react/LoadingSpinner.tsx
Normal file
3
apps/client/src/widgets/react/LoadingSpinner.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function LoadingSpinner() {
|
||||
return <span className="bx bx-loader bx-spin" />
|
||||
}
|
84
apps/client/src/widgets/ribbon/NoteInfoTab.tsx
Normal file
84
apps/client/src/widgets/ribbon/NoteInfoTab.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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" }}>
|
||||
|
@ -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 */
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user