diff --git a/src/components/StatsDrawer.tsx b/src/components/StatsDrawer.tsx index e2841fb..2ce0bbf 100644 --- a/src/components/StatsDrawer.tsx +++ b/src/components/StatsDrawer.tsx @@ -1,10 +1,11 @@ -import React, { useState, useEffect } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import renderOverlay, { RoamOverlayProps, } from "roamjs-components/util/renderOverlay"; import getCurrentUserEmail from "roamjs-components/queries/getCurrentUserEmail"; import getCurrentUserDisplayName from "roamjs-components/queries/getCurrentUserDisplayName"; import { + Button, Card, Classes, Drawer, @@ -18,6 +19,20 @@ import { } from "@blueprintjs/core"; import { scalar } from "~/utils/scalar"; +import { + formatInt, + getAutoLoadPreference, + getLoadingKeyForTag, + runAfterNextPaint, + TAGS, +} from "~/utils/stats"; +import type { + ExtensionAPI, + LoadingState, + StatMetricKey, + Stats, + TagName, +} from "~/types/stats"; import { runQuery, queryPages, @@ -35,38 +50,26 @@ import { queryExternalLinks, } from "~/utils/queries"; -const TAGS = [ - "TODO", - "DONE", - "query", - "embed", - "table", - "kanban", - "video", - "roam/js", -]; - -type Stats = Partial<{ - pages: number; - nonCodeBlocks: number; - nonCodeBlockWords: number; - nonCodeBlockChars: number; - blockquotes: number; - blockquotesWords: number; - blockquotesChars: number; - codeBlocks: number; - codeBlockChars: number; - interconnections: number; - tagCounts: Record; - firebaseLinks: number; - externalLinks: number; -}>; +const STAT_QUERIES: Record = { + pages: queryPages, + nonCodeBlocks: queryNonCodeBlocks, + nonCodeBlockWords: queryNonCodeBlockWords, + nonCodeBlockChars: queryNonCodeBlockCharacters, + blockquotes: queryBlockquotes, + blockquotesWords: queryBlockquotesWords, + blockquotesChars: queryBlockquotesCharacters, + codeBlocks: queryCodeBlocks, + codeBlockChars: queryCodeBlockCharacters, + interconnections: queryInterconnections, + firebaseLinks: queryFireBaseAttachements, + externalLinks: queryExternalLinks, +}; -const STATS_DRAWER_ID = "roamjs-stats-drawer"; +const STAT_METRIC_KEYS = Object.keys(STAT_QUERIES) as StatMetricKey[]; -const formatInt = (n: number) => n.toLocaleString(); +const STATS_DRAWER_ID = "roamjs-stats-drawer"; -const Loader = () => ( +const Loader = (): React.ReactElement => ( @@ -78,7 +81,7 @@ type MetricProps = { icon: React.ComponentProps["icon"]; }; -const Metric = ({ label, value, icon }: MetricProps) => ( +const Metric = ({ label, value, icon }: MetricProps): React.ReactElement => (
@@ -88,57 +91,194 @@ const Metric = ({ label, value, icon }: MetricProps) => (
); -export const StatsDrawer = ({ onClose, isOpen }: RoamOverlayProps<{}>) => { +type RenderStatValueArgs = { + statKey: StatMetricKey; + stats: Stats; + loading: LoadingState; + autoLoad: boolean; + onLoad: ({ statKey }: { statKey: StatMetricKey }) => void; +}; + +const renderStatValue = ({ + statKey, + stats, + loading, + autoLoad, + onLoad, +}: RenderStatValueArgs): React.ReactNode => { + const value = stats[statKey]; + if (value != null) { + return formatInt(value); + } + + if (loading[statKey]) { + return ; + } + + if (!autoLoad) { + return ( +
+ + )} {/* Top overview */}
- } + value={renderStatValue({ + statKey: "pages", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} icon="document" /> - ) - } + value={renderStatValue({ + statKey: "interconnections", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} icon="link" /> - ) - } + value={renderStatValue({ + statKey: "firebaseLinks", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} icon="cloud" /> - ) - } + value={renderStatValue({ + statKey: "externalLinks", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} icon="share" /> @@ -229,11 +390,13 @@ export const StatsDrawer = ({ onClose, isOpen }: RoamOverlayProps<{}>) => { Blocks - {stats.nonCodeBlocks != null ? ( - formatInt(stats.nonCodeBlocks) - ) : ( - - )} + {renderStatValue({ + statKey: "nonCodeBlocks", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} @@ -241,11 +404,13 @@ export const StatsDrawer = ({ onClose, isOpen }: RoamOverlayProps<{}>) => { Words - {stats.nonCodeBlockWords != null ? ( - formatInt(stats.nonCodeBlockWords) - ) : ( - - )} + {renderStatValue({ + statKey: "nonCodeBlockWords", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} @@ -253,11 +418,13 @@ export const StatsDrawer = ({ onClose, isOpen }: RoamOverlayProps<{}>) => { Characters - {stats.nonCodeBlockChars != null ? ( - formatInt(stats.nonCodeBlockChars) - ) : ( - - )} + {renderStatValue({ + statKey: "nonCodeBlockChars", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} @@ -276,11 +443,13 @@ export const StatsDrawer = ({ onClose, isOpen }: RoamOverlayProps<{}>) => { Quotes - {stats.blockquotes != null ? ( - formatInt(stats.blockquotes) - ) : ( - - )} + {renderStatValue({ + statKey: "blockquotes", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} @@ -288,11 +457,13 @@ export const StatsDrawer = ({ onClose, isOpen }: RoamOverlayProps<{}>) => { Words - {stats.blockquotesWords != null ? ( - formatInt(stats.blockquotesWords) - ) : ( - - )} + {renderStatValue({ + statKey: "blockquotesWords", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} @@ -300,11 +471,13 @@ export const StatsDrawer = ({ onClose, isOpen }: RoamOverlayProps<{}>) => { Characters - {stats.blockquotesChars != null ? ( - formatInt(stats.blockquotesChars) - ) : ( - - )} + {renderStatValue({ + statKey: "blockquotesChars", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} @@ -323,11 +496,13 @@ export const StatsDrawer = ({ onClose, isOpen }: RoamOverlayProps<{}>) => { Blocks - {stats.codeBlocks != null ? ( - formatInt(stats.codeBlocks) - ) : ( - - )} + {renderStatValue({ + statKey: "codeBlocks", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} @@ -335,11 +510,13 @@ export const StatsDrawer = ({ onClose, isOpen }: RoamOverlayProps<{}>) => { Characters - {stats.codeBlockChars != null ? ( - formatInt(stats.codeBlockChars) - ) : ( - - )} + {renderStatValue({ + statKey: "codeBlockChars", + stats, + loading, + autoLoad, + onLoad: loadMetric, + })} @@ -363,20 +540,27 @@ export const StatsDrawer = ({ onClose, isOpen }: RoamOverlayProps<{}>) => { ? Intent.SUCCESS : Intent.NONE } - onClick={() => + onClick={() => { + if (!autoLoad && stats.tagCounts?.[tag] == null) { + loadTag({ tag }); + return; + } + window.roamAlphaAPI.ui.mainWindow.openPage({ page: { title: tag }, - }) - } + }); + }} className="cursor-pointer" > {tag}{" "} - {stats.tagCounts?.[tag] != null ? ( - formatInt(stats.tagCounts[tag]) - ) : ( - - )} + {renderTagValue({ + tag, + stats, + loading, + autoLoad, + onLoad: loadTag, + })} ))} @@ -411,7 +595,11 @@ export const StatsDrawer = ({ onClose, isOpen }: RoamOverlayProps<{}>) => { let closeStatsDrawer: (() => void) | null = null; -export const toggleStatsDrawer = () => { +export const toggleStatsDrawer = ({ + extensionAPI, +}: { + extensionAPI: ExtensionAPI; +}): void => { if (document.getElementById(STATS_DRAWER_ID)) { closeStatsDrawer?.(); closeStatsDrawer = null; @@ -420,6 +608,7 @@ export const toggleStatsDrawer = () => { renderOverlay({ id: STATS_DRAWER_ID, Overlay: StatsDrawer, + props: { extensionAPI }, }) ?? null; } }; diff --git a/src/index.ts b/src/index.ts index 9a7e570..2ad9326 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,18 @@ import runExtension from "roamjs-components/util/runExtension"; +import type { ExtensionAPI } from "~/types/stats"; +import { AUTO_LOAD_STATS_SETTING } from "~/utils/stats"; import { toggleStatsDrawer } from "./components/StatsDrawer"; import { render as renderToast } from "roamjs-components/components/Toast"; const COMMAND_LABEL = "Stats: Toggle Stats Drawer"; +const SETTINGS_TAB_TITLE = "Stats"; + +const getAutoLoadSettingValue = ({ + extensionAPI, +}: { + extensionAPI: ExtensionAPI; +}): unknown => extensionAPI.settings.get(AUTO_LOAD_STATS_SETTING); + export default runExtension(async ({ extensionAPI }) => { if (process.env.NODE_ENV === "development") { renderToast({ @@ -13,9 +23,25 @@ export default runExtension(async ({ extensionAPI }) => { }); } + extensionAPI.settings.panel.create({ + tabTitle: SETTINGS_TAB_TITLE, + settings: [ + { + id: AUTO_LOAD_STATS_SETTING, + name: "Auto-load stats", + description: "Automatically fetch all stats when opening the drawer.", + action: { type: "switch" }, + }, + ], + }); + + if (typeof getAutoLoadSettingValue({ extensionAPI }) !== "boolean") { + await extensionAPI.settings.set(AUTO_LOAD_STATS_SETTING, true); + } + await extensionAPI.ui.commandPalette.addCommand({ label: COMMAND_LABEL, - callback: toggleStatsDrawer, + callback: () => toggleStatsDrawer({ extensionAPI }), }); return { diff --git a/src/types/stats.ts b/src/types/stats.ts new file mode 100644 index 0000000..9d0db15 --- /dev/null +++ b/src/types/stats.ts @@ -0,0 +1,33 @@ +import type { OnloadArgs } from "roamjs-components/types/native"; + +export type ExtensionAPI = OnloadArgs["extensionAPI"]; + +export type TagName = + | "TODO" + | "DONE" + | "query" + | "embed" + | "table" + | "kanban" + | "video" + | "roam/js"; + +export type StatMetricKey = + | "pages" + | "nonCodeBlocks" + | "nonCodeBlockWords" + | "nonCodeBlockChars" + | "blockquotes" + | "blockquotesWords" + | "blockquotesChars" + | "codeBlocks" + | "codeBlockChars" + | "interconnections" + | "firebaseLinks" + | "externalLinks"; + +export type Stats = Partial> & { + tagCounts?: Partial>; +}; + +export type LoadingState = Record; diff --git a/src/utils/stats.ts b/src/utils/stats.ts new file mode 100644 index 0000000..f0c8e76 --- /dev/null +++ b/src/utils/stats.ts @@ -0,0 +1,40 @@ +import type { ExtensionAPI, TagName } from "~/types/stats"; + +export const TAGS: readonly TagName[] = [ + "TODO", + "DONE", + "query", + "embed", + "table", + "kanban", + "video", + "roam/js", +]; + +export const AUTO_LOAD_STATS_SETTING = "auto-load-stats"; + +export const formatInt = (n: number): string => n.toLocaleString(); + +export const getLoadingKeyForTag = (tag: TagName): string => `tag:${tag}`; + +export const runAfterNextPaint = ({ + callback, +}: { + callback: () => void; +}): void => { + if (typeof window.requestAnimationFrame === "function") { + window.requestAnimationFrame(() => callback()); + return; + } + + window.setTimeout(() => callback(), 0); +}; + +export const getAutoLoadPreference = ({ + extensionAPI, +}: { + extensionAPI: ExtensionAPI; +}): boolean => { + const value = extensionAPI.settings.get(AUTO_LOAD_STATS_SETTING); + return typeof value === "boolean" ? value : true; +};