diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index 924e77862..ea9163bd8 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -30,9 +30,10 @@ import { notify, subscribe, } from "~/utils/discourseConfigRef"; -import type { - LeftSidebarConfig, - LeftSidebarPersonalSectionConfig, +import { + isQuerySection, + type LeftSidebarConfig, + type LeftSidebarPersonalSectionConfig, } from "~/utils/getLeftSidebarSettings"; import { createBlock } from "roamjs-components/writes"; import deleteBlock from "roamjs-components/writes/deleteBlock"; @@ -48,6 +49,7 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU import { migrateLeftSidebarSettings } from "~/utils/migrateLeftSidebarSettings"; import posthog from "posthog-js"; import { commands, cleanCommandName } from "~/components/LeftSidebarCommands"; +import runQuery from "~/utils/runQuery"; const parseReference = (text: string) => { const extracted = extractRef(text); @@ -289,6 +291,151 @@ const PersonalSectionItem = ({ ); }; +const QuerySectionItem = ({ + section, + dragHandle, + onloadArgs, +}: { + section: LeftSidebarPersonalSectionConfig; + dragHandle: SortableHandle; + onloadArgs: OnloadArgs; +}) => { + const queryUid = extractRef(section.text); + const alias = section.settings?.alias?.value; + const queryLabel = useMemo(() => getTextByBlockUid(queryUid), [queryUid]); + const displayName = alias || queryLabel || section.text; + const truncateAt = section.settings?.truncateResult.value; + const resultLimit = section.settings?.resultLimit?.value ?? 0; + + const [isOpen, setIsOpen] = useState( + !!section.settings?.folded.value || false, + ); + const [results, setResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [hasLoaded, setHasLoaded] = useState(false); + const [error, setError] = useState(null); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const loadResults = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + const { allProcessedResults } = await runQuery({ + parentUid: queryUid, + extensionAPI: onloadArgs.extensionAPI, + }); + const children: ChildNode[] = allProcessedResults.map((r) => ({ + uid: r.uid, + text: getPageTitleByPageUid(r.uid) ? r.uid : `((${r.uid}))`, + })); + setResults(children); + } catch (e) { + console.error(e); + setError("Query failed to run"); + } finally { + setIsLoading(false); + setHasLoaded(true); + } + }, [queryUid, onloadArgs.extensionAPI]); + + useEffect(() => { + if (isOpen && !hasLoaded) { + void loadResults(); + } + }, [isOpen, hasLoaded, loadResults]); + + const handleChevronClick = () => { + if (!section.settings) return; + toggleFoldedState({ + isOpen, + setIsOpen, + folded: section.settings.folded, + parentUid: section.settings.uid || "", + }); + }; + + return ( + <> +
+
+
+ {displayName.toUpperCase()} +
+ setIsMenuOpen(next)} + onClose={() => setIsMenuOpen(false)} + popoverClassName="dg-leftsidebar-popover" + minimal + content={ + + { + void loadResults(); + setIsMenuOpen(false); + }} + /> + { + void window.roamAlphaAPI.ui.mainWindow.openBlock({ + block: { uid: queryUid }, + }); + setIsMenuOpen(false); + }} + /> + + } + > + + + + + + + +
+
+ + {isLoading ? ( +
Loading…
+ ) : error ? ( +
{error}
+ ) : results.length > 0 ? ( + 0 ? results.slice(0, resultLimit) : results + } + truncateAt={truncateAt} + onloadArgs={onloadArgs} + /> + ) : hasLoaded ? ( +
No results
+ ) : null} +
+ + ); +}; + const PersonalSections = ({ config, setConfig, @@ -358,14 +505,22 @@ const PersonalSections = ({ getId={(s) => s.uid} onReorder={reorderSections} className="personal-left-sidebar-sections" - renderItem={(section, handle) => ( - - )} + renderItem={(section, handle) => + isQuerySection(section.text) ? ( + + ) : ( + + ) + } /> ); }; diff --git a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx index 449d51c59..a702375fe 100644 --- a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx @@ -46,6 +46,7 @@ import { commands, SidebarCommandPopover, } from "~/components/LeftSidebarCommands"; +import { isQuerySection } from "~/utils/getLeftSidebarSettings"; /* eslint-disable @typescript-eslint/naming-convention */ export const sectionsToBlockProps = ( @@ -60,6 +61,8 @@ export const sectionsToBlockProps = ( Settings: { "Truncate-result?": s.settings?.truncateResult?.value ?? 75, Folded: s.settings?.folded?.value ?? false, + Alias: s.settings?.alias?.value ?? "", + "Result-limit": s.settings?.resultLimit?.value ?? 0, }, })); /* eslint-enable @typescript-eslint/naming-convention */ @@ -98,6 +101,11 @@ const SectionItem = memo( new Set(initiallyExpanded ? [section.uid] : []), ); const isExpanded = expandedChildLists.has(section.uid); + const isQuery = isQuerySection(section.text); + const [aliasValue, setAliasValue] = useState( + section.settings?.alias?.value ?? "", + ); + const aliasUpdateTimeoutRef = useRef>(); const [childSettingsUid, setChildSettingsUid] = useState( null, ); @@ -328,6 +336,54 @@ const SectionItem = memo( setChildInputKey((prev) => prev + 1); }, []); + const handleAliasChange = useCallback( + (newValue: string) => { + setAliasValue(newValue); + + clearTimeout(aliasUpdateTimeoutRef.current); + aliasUpdateTimeoutRef.current = setTimeout(() => { + const currentSection = sectionsRef.current.find( + (s) => s.uid === section.uid, + ); + const alias = currentSection?.settings?.alias; + if (!alias?.uid) return; + const aliasUid = alias.uid; + + void (async () => { + let valueUid = alias.valueUid; + if (valueUid) { + await updateBlock({ uid: valueUid, text: newValue }); + } else { + valueUid = await createBlock({ + parentUid: aliasUid, + order: 0, + node: { text: newValue }, + }); + } + const nextSections = sectionsRef.current.map((s) => + s.uid === section.uid && s.settings + ? { + ...s, + settings: { + ...s.settings, + alias: { + ...s.settings.alias, + valueUid, + value: newValue, + }, + }, + } + : s, + ); + setSections(nextSections); + syncAllSectionsToBlockProps(nextSections); + refreshAndNotify(); + })(); + }, 300); + }, + [section.uid, sectionsRef, setSections], + ); + const handleAddChild = useCallback(async () => { if (childInput && section.childrenUid) { await addChildToSection(section, section.childrenUid, childInput); @@ -340,6 +396,45 @@ const SectionItem = memo( (!section.settings && section.children?.length === 0) || !section.children; + if (isQuery) { + return ( +
+
+ handleAliasChange(e.target.value)} + placeholder="Alias…" + small + /> + + {section.text} + +
+ +
+
+ ); + } + return (
+ { + const updatedSections = sectionsRef.current.map((s) => + s.uid === activeDialogSection.uid + ? { + ...s, + settings: s.settings + ? { + ...s.settings, + resultLimit: { + ...s.settings.resultLimit, + value, + }, + } + : s.settings, + } + : s, + ); + setSections(updatedSections); + syncAllSectionsToBlockProps(updatedSections); + }} + />
diff --git a/apps/roam/src/utils/getLeftSidebarSettings.ts b/apps/roam/src/utils/getLeftSidebarSettings.ts index 94d018690..c282efee9 100644 --- a/apps/roam/src/utils/getLeftSidebarSettings.ts +++ b/apps/roam/src/utils/getLeftSidebarSettings.ts @@ -9,12 +9,19 @@ import { } from "./getExportSettings"; import { getSubTree } from "roamjs-components/util"; +type AliasSetting = StringSetting & { valueUid?: string }; + type LeftSidebarPersonalSectionSettings = { uid: string; truncateResult: IntSetting; folded: BooleanSetting; + alias?: AliasSetting; + resultLimit?: IntSetting; }; +const BLOCK_REF_PATTERN = /^\(\([\w-]{9,10}\)\)$/; +export const isQuerySection = (text: string) => BLOCK_REF_PATTERN.test(text); + export type PersonalSectionChild = RoamBasicNode & { alias: StringSetting; }; @@ -119,10 +126,24 @@ const getPersonalSectionSettings = ( text: "Folded", }); + const aliasNode = settingsTree.find((n) => n.text === "Alias"); + const aliasSetting: AliasSetting = { + uid: aliasNode?.uid, + value: aliasNode?.children?.[0]?.text ?? "", + valueUid: aliasNode?.children?.[0]?.uid, + }; + + const resultLimitSetting = getUidAndIntSetting({ + tree: settingsTree, + text: "Result-limit", + }); + return { uid: settingsNode.uid, truncateResult: truncateResultSetting, folded: foldedSetting, + alias: aliasSetting, + resultLimit: resultLimitSetting, }; };