diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index bd924768f..7ae1899fe 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -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) { diff --git a/apps/client/src/utils/formatters.ts b/apps/client/src/utils/formatters.ts index 755cc8153..d3209be7b 100644 --- a/apps/client/src/utils/formatters.ts +++ b/apps/client/src/utils/formatters.ts @@ -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; diff --git a/apps/client/src/widgets/react/LoadingSpinner.tsx b/apps/client/src/widgets/react/LoadingSpinner.tsx new file mode 100644 index 000000000..b3eb99e2e --- /dev/null +++ b/apps/client/src/widgets/react/LoadingSpinner.tsx @@ -0,0 +1,3 @@ +export default function LoadingSpinner() { + return +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/NoteInfoTab.tsx b/apps/client/src/widgets/ribbon/NoteInfoTab.tsx new file mode 100644 index 000000000..466410b26 --- /dev/null +++ b/apps/client/src/widgets/ribbon/NoteInfoTab.tsx @@ -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(); + const [ isLoading, setIsLoading ] = useState(false); + const [ noteSizeResponse, setNoteSizeResponse ] = useState(); + const [ subtreeSizeResponse, setSubtreeSizeResponse ] = useState(); + + useEffect(() => { + if (note) { + server.get(`notes/${note?.noteId}/metadata`).then(setMetadata); + } + + setNoteSizeResponse(undefined); + setSubtreeSizeResponse(undefined); + setIsLoading(false); + }, [ note?.noteId ]); + + console.log("Got ", noteSizeResponse, subtreeSizeResponse); + + return ( +
+ {note && ( + + + + + + + + + + + + + + + + + +
{t("note_info_widget.note_id")}:{note.noteId}{t("note_info_widget.created")}:{formatDateTime(metadata?.dateCreated)}{t("note_info_widget.modified")}:{formatDateTime(metadata?.dateModified)}
{t("note_info_widget.type")}: + {note.type}{' '} + { note.mime && ({note.mime}) } + {t("note_info_widget.note_size")}: + {!isLoading && !noteSizeResponse && !subtreeSizeResponse && ( +
+ )} +
+ ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/Ribbon.tsx b/apps/client/src/widgets/ribbon/Ribbon.tsx index 5dbeb1c62..abe0e1fdd 100644 --- a/apps/client/src/widgets/ribbon/Ribbon.tsx +++ b/apps/client/src/widgets/ribbon/Ribbon.tsx @@ -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([ 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(); - 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 (
diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css index 514ec98a4..678c24af7 100644 --- a/apps/client/src/widgets/ribbon/style.css +++ b/apps/client/src/widgets/ribbon/style.css @@ -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 */ \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon_widgets/note_info_widget.ts b/apps/client/src/widgets/ribbon_widgets/note_info_widget.ts deleted file mode 100644 index 3a390eb1d..000000000 --- a/apps/client/src/widgets/ribbon_widgets/note_info_widget.ts +++ /dev/null @@ -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*/` -
- - - - - - - - - - - - - - - - - - -
${t("note_info_widget.note_id")}:${t("note_info_widget.created")}:${t("note_info_widget.modified")}:
${t("note_info_widget.type")}: - - - ${t("note_info_widget.note_size")}: - - - - - -
-
-`; - -// 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; - private $dateCreated!: JQuery; - private $dateModified!: JQuery; - private $type!: JQuery; - private $mime!: JQuery; - private $noteSizesWrapper!: JQuery; - private $noteSize!: JQuery; - private $subTreeSize!: JQuery; - private $calculateButton!: JQuery; - - 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($('')); - this.$subTreeSize.empty().append($('')); - - const noteSizeResp = await server.get(`stats/note-size/${this.noteId}`); - this.$noteSize.text(utils.formatSize(noteSizeResp.noteSize)); - - const subTreeResp = await server.get(`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(`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(); - } - } -} diff --git a/apps/server/src/routes/api/notes.ts b/apps/server/src/routes/api/notes.ts index fd938dc18..f481d199a 100644 --- a/apps/server/src/routes/api/notes.ts +++ b/apps/server/src/routes/api/notes.ts @@ -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) { diff --git a/apps/server/src/routes/api/stats.ts b/apps/server/src/routes/api/stats.ts index 15e28f083..aebca079e 100644 --- a/apps/server/src/routes/api/stats.ts +++ b/apps/server/src/routes/api/stats.ts @@ -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 { diff --git a/packages/commons/src/lib/server_api.ts b/packages/commons/src/lib/server_api.ts index 199ab164b..46b403d0f 100644 --- a/packages/commons/src/lib/server_api.ts +++ b/packages/commons/src/lib/server_api.ts @@ -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; +}