diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index 14305fd1b..59ff81bd0 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -30,11 +30,13 @@ import { settingKeys, } from "~/components/settings/utils/settingsEmitter"; import { + isQueryBlockRef, type LeftSidebarConfig, type LeftSidebarPersonalSectionConfig, mergeGlobalSectionWithAccessor, mergePersonalSectionsWithAccessor, } from "~/utils/getLeftSidebarSettings"; +import runQuery from "~/utils/runQuery"; import { sectionsToBlockProps } from "./settings/LeftSidebarPersonalSettings"; import discourseConfigRef, { notify } from "~/utils/discourseConfigRef"; import { getLeftSidebarSettings } from "~/utils/getLeftSidebarSettings"; @@ -362,6 +364,171 @@ const PersonalSectionItem = ({ ); }; +const QuerySectionItem = ({ + section, + sectionIndex, + dragHandle, + onloadArgs, +}: { + section: LeftSidebarPersonalSectionConfig; + sectionIndex: number; + 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 = Math.max( + 0, + Math.trunc(section.settings?.resultLimit?.value ?? 10), + ); + + const [isOpen, setIsOpen] = useState( + !!section.settings?.folded.value, + ); + 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 isTogglingRef = useRef(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) => { + const isPage = !!getPageTitleByPageUid(r.uid); + return { + uid: r.uid, + text: isPage ? 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 = async () => { + if (!section.settings) return; + if (isTogglingRef.current) return; + isTogglingRef.current = true; + try { + await toggleFoldedState({ + isOpen, + setIsOpen, + folded: section.settings.folded, + parentUid: section.settings.uid || "", + sectionIndex, + }); + } finally { + isTogglingRef.current = false; + } + }; + + const limitedResults = + resultLimit > 0 ? results.slice(0, resultLimit) : results; + + let body: React.ReactNode = null; + if (isLoading) { + body =
Loading…
; + } else if (error) { + body =
{error}
; + } else if (limitedResults.length > 0) { + body = limitedResults.map((child) => ( + + )); + } else if (hasLoaded) { + body =
No results
; + } + + return ( + <> +
+
+
void handleChevronClick()} + > + {displayName.toUpperCase()} +
+ void handleChevronClick()} + > + + + 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); + }} + /> + + } + > + + + + +
+
+ {body} + + ); +}; + const PersonalSections = ({ config, setConfig, @@ -439,15 +606,28 @@ const PersonalSections = ({ getId={(s) => s.uid} onReorder={reorderSections} className="personal-left-sidebar-sections" - renderItem={(section, handle) => ( - s.uid === section.uid)} - dragHandle={handle} - onChildrenReorder={reorderChildren} - onloadArgs={onloadArgs} - /> - )} + renderItem={(section, handle) => { + const sectionIndex = sections.findIndex((s) => s.uid === section.uid); + if (isQueryBlockRef(section.text) && section.settings?.uid) { + return ( + + ); + } + return ( + + ); + }} /> ); }; diff --git a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx index df95487cc..c7e34d9b5 100644 --- a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx @@ -30,6 +30,7 @@ import { PersonalTextPanel, } from "~/components/settings/components/BlockPropSettingPanels"; import { + isQueryBlockRef, LeftSidebarPersonalSectionConfig, getLeftSidebarPersonalSectionConfig, mergePersonalSectionsWithAccessor, @@ -71,6 +72,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 ?? 10, }, })); /* eslint-enable @typescript-eslint/naming-convention */ @@ -112,9 +115,22 @@ const SectionItem = memo( new Set(initiallyExpanded ? [section.uid] : []), ); const isExpanded = expandedChildLists.has(section.uid); + const isQuery = isQueryBlockRef(section.text); + const [aliasValue, setAliasValue] = useState( + section.settings?.alias?.value ?? "", + ); + const aliasUpdateTimeoutRef = useRef>(); const [childSettingsUid, setChildSettingsUid] = useState( null, ); + + useEffect(() => { + return () => { + clearTimeout(aliasUpdateTimeoutRef.current); + aliasUpdateTimeoutRef.current = undefined; + }; + }, []); + const toggleChildrenList = useCallback((sectionUid: string) => { setExpandedChildLists((prev) => { const next = new Set(prev); @@ -337,6 +353,84 @@ 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, + ); + if (!currentSection?.uid) return; + + void (async () => { + let settingsUid = currentSection.settings?.uid; + if (!settingsUid) { + settingsUid = await createBlock({ + parentUid: currentSection.uid, + order: 0, + node: { text: "Settings" }, + }); + } + + let aliasUid = currentSection.settings?.alias?.uid; + if (!aliasUid) { + aliasUid = await createBlock({ + parentUid: settingsUid, + order: 0, + node: { text: "Alias" }, + }); + } + + let valueUid = currentSection.settings?.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: { + uid: settingsUid, + folded: s.settings?.folded ?? { + uid: undefined, + value: false, + }, + truncateResult: s.settings?.truncateResult ?? { + uid: undefined, + value: 75, + }, + alias: { + ...(s.settings?.alias ?? {}), + uid: aliasUid, + valueUid, + value: newValue, + }, + resultLimit: s.settings?.resultLimit ?? { + uid: undefined, + value: 10, + }, + }, + } + : s, + ); + sectionsRef.current = nextSections; + setSections(nextSections); + syncAllSectionsToBlockProps(nextSections); + refreshAndNotify(); + })(); + }, 300); + }, + [section.uid, sectionsRef, setSections], + ); + const handleAddChild = useCallback(async () => { if (childInput && section.childrenUid) { await addChildToSection(section, section.childrenUid, childInput); @@ -349,6 +443,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/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 083720d8c..10085df6c 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -254,6 +254,8 @@ const getLegacyPersonalLeftSidebarSetting = (): unknown[] => { Settings: { "Truncate-result?": section.settings?.truncateResult.value ?? 75, Folded: section.settings?.folded.value ?? false, + Alias: section.settings?.alias?.value ?? "", + "Result-limit": section.settings?.resultLimit?.value ?? 0, }, })); /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/apps/roam/src/components/settings/utils/zodSchema.example.ts b/apps/roam/src/components/settings/utils/zodSchema.example.ts index 718a20929..0b32304b5 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.example.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.example.ts @@ -286,6 +286,8 @@ const personalSection: PersonalSection = { Settings: { "Truncate-result?": 100, Folded: false, + Alias: "", + "Result-limit": 10, }, }; @@ -299,6 +301,8 @@ const leftSidebarPersonalSettings: LeftSidebarPersonalSettings = [ Settings: { "Truncate-result?": 75, Folded: false, + Alias: "", + "Result-limit": 10, }, }, { @@ -311,6 +315,8 @@ const leftSidebarPersonalSettings: LeftSidebarPersonalSettings = [ Settings: { "Truncate-result?": 50, Folded: true, + Alias: "", + "Result-limit": 10, }, }, ]; @@ -347,6 +353,8 @@ const personalSettings: PersonalSettings = { Settings: { "Truncate-result?": 75, Folded: false, + Alias: "", + "Result-limit": 10, }, }, { @@ -358,6 +366,8 @@ const personalSettings: PersonalSettings = { Settings: { "Truncate-result?": 50, Folded: true, + Alias: "", + "Result-limit": 10, }, }, ], diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts index 733493cbf..917b518de 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -221,6 +221,8 @@ export const PersonalSectionSchema = z.object({ .object({ "Truncate-result?": z.number().default(75), Folded: z.boolean().default(false), + Alias: z.string().default(""), + "Result-limit": z.number().int().min(0).default(10), }) .default({}), }); diff --git a/apps/roam/src/utils/getExportSettings.ts b/apps/roam/src/utils/getExportSettings.ts index 06284a9a5..410493eb5 100644 --- a/apps/roam/src/utils/getExportSettings.ts +++ b/apps/roam/src/utils/getExportSettings.ts @@ -81,6 +81,20 @@ export const getUidAndStringSetting = (props: Props): StringSetting => { }; }; +export type StringSettingWithValueUid = StringSetting & { valueUid?: string }; + +export const getUidAndStringSettingWithValueUid = ( + props: Props, +): StringSettingWithValueUid => { + const node = props.tree.find((node) => node.text === props.text); + const valueChild = node?.children?.[0]; + return { + uid: node?.uid, + value: valueChild?.text ?? "", + valueUid: valueChild?.uid, + }; +}; + export const getExportSettingsAndUids = ( configTreeOverride?: RoamBasicNode[], ): ExportConfigWithUids => { diff --git a/apps/roam/src/utils/getLeftSidebarSettings.ts b/apps/roam/src/utils/getLeftSidebarSettings.ts index af4e7a53c..0d3cab4b2 100644 --- a/apps/roam/src/utils/getLeftSidebarSettings.ts +++ b/apps/roam/src/utils/getLeftSidebarSettings.ts @@ -1,11 +1,16 @@ import { RoamBasicNode } from "roamjs-components/types"; +import { BLOCK_REF_REGEX } from "roamjs-components/dom/constants"; +import extractRef from "roamjs-components/util/extractRef"; +import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; import { BooleanSetting, getUidAndBooleanSetting, IntSetting, getUidAndIntSetting, StringSetting, + StringSettingWithValueUid, getUidAndStringSetting, + getUidAndStringSettingWithValueUid, } from "./getExportSettings"; import { getSubTree } from "roamjs-components/util"; import type { @@ -17,6 +22,19 @@ type LeftSidebarPersonalSectionSettings = { uid: string; truncateResult: IntSetting; folded: BooleanSetting; + alias?: StringSettingWithValueUid; + resultLimit?: IntSetting; +}; + +const BLOCK_REF_FULL_MATCH = new RegExp(`^${BLOCK_REF_REGEX.source}$`); +const QUERY_BLOCK_MARKER = /\{\{query block(?::[^}]*)?\}\}/; + +export const isQueryBlockRef = (text: string): boolean => { + if (!BLOCK_REF_FULL_MATCH.test(text)) return false; + const blockText = getTextByBlockUid(extractRef(text)); + if (!blockText) return false; + if (blockText.includes(":SmartBlock:")) return false; + return QUERY_BLOCK_MARKER.test(blockText); }; export type PersonalSectionChild = RoamBasicNode & { @@ -123,10 +141,23 @@ const getPersonalSectionSettings = ( text: "Folded", }); + const aliasSetting = getUidAndStringSettingWithValueUid({ + tree: settingsTree, + text: "Alias", + }); + + const resultLimitSetting = getUidAndIntSetting({ + tree: settingsTree, + text: "Result-limit", + defaultValue: 10, + }); + return { uid: settingsNode.uid, truncateResult: truncateResultSetting, folded: foldedSetting, + alias: aliasSetting, + resultLimit: resultLimitSetting, }; }; @@ -267,6 +298,15 @@ export const mergePersonalSectionsWithAccessor = ( uid: legacy?.settings?.folded.uid ?? "", value: snap.Settings.Folded, }, + alias: { + uid: legacy?.settings?.alias?.uid, + valueUid: legacy?.settings?.alias?.valueUid, + value: snap.Settings.Alias, + }, + resultLimit: { + uid: legacy?.settings?.resultLimit?.uid, + value: snap.Settings["Result-limit"], + }, }, children: snap.Children.length > 0