mirror of
https://github.com/zadam/trilium.git
synced 2025-10-21 07:38:53 +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('"', """);
|
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);
|
size = Math.max(Math.round(size / 1024), 1);
|
||||||
|
|
||||||
if (size < 1024) {
|
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.
|
* 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;
|
const locale = navigator.language;
|
||||||
|
|
||||||
let parsedDate;
|
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 ScriptTab from "./ScriptTab";
|
||||||
import EditedNotesTab from "./EditedNotesTab";
|
import EditedNotesTab from "./EditedNotesTab";
|
||||||
import NotePropertiesTab from "./NotePropertiesTab";
|
import NotePropertiesTab from "./NotePropertiesTab";
|
||||||
|
import NoteInfoTab from "./NoteInfoTab";
|
||||||
|
|
||||||
interface TitleContext {
|
interface TitleContext {
|
||||||
note: FNote | null | undefined;
|
note: FNote | null | undefined;
|
||||||
@ -118,9 +119,11 @@ const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>([
|
|||||||
icon: "bx bx-bar-chart"
|
icon: "bx bx-bar-chart"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// NoteInfoWidget
|
|
||||||
title: t("note_info_widget.title"),
|
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 { note } = useNoteContext();
|
||||||
const titleContext: TitleContext = { note };
|
const titleContext: TitleContext = { note };
|
||||||
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
|
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 (
|
return (
|
||||||
<div class="ribbon-container" style={{ contain: "none" }}>
|
<div class="ribbon-container" style={{ contain: "none" }}>
|
||||||
|
@ -159,3 +159,27 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #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 blobService from "../../services/blob.js";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
import type BBranch from "../../becca/entities/bbranch.js";
|
import type BBranch from "../../becca/entities/bbranch.js";
|
||||||
import type { AttributeRow, DeleteNotesPreview } from "@triliumnext/commons";
|
import type { AttributeRow, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
@ -101,7 +101,7 @@ function getNoteMetadata(req: Request) {
|
|||||||
utcDateCreated: note.utcDateCreated,
|
utcDateCreated: note.utcDateCreated,
|
||||||
dateModified: note.dateModified,
|
dateModified: note.dateModified,
|
||||||
utcDateModified: note.utcDateModified
|
utcDateModified: note.utcDateModified
|
||||||
};
|
} satisfies MetadataResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNote(req: Request) {
|
function createNote(req: Request) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import sql from "../../services/sql.js";
|
import sql from "../../services/sql.js";
|
||||||
import becca from "../../becca/becca.js";
|
import becca from "../../becca/becca.js";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
|
import { NoteSizeResponse, SubtreeSizeResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
function getNoteSize(req: Request) {
|
function getNoteSize(req: Request) {
|
||||||
const { noteId } = req.params;
|
const { noteId } = req.params;
|
||||||
@ -22,7 +23,7 @@ function getNoteSize(req: Request) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
noteSize
|
noteSize
|
||||||
};
|
} satisfies NoteSizeResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubtreeSize(req: Request) {
|
function getSubtreeSize(req: Request) {
|
||||||
@ -45,7 +46,7 @@ function getSubtreeSize(req: Request) {
|
|||||||
return {
|
return {
|
||||||
subTreeSize,
|
subTreeSize,
|
||||||
subTreeNoteCount: subTreeNoteIds.length
|
subTreeNoteCount: subTreeNoteIds.length
|
||||||
};
|
} satisfies SubtreeSizeResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -169,3 +169,19 @@ export type EditedNotesResponse = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
notePath?: string[] | null;
|
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