Skip to content

Commit 616fafa

Browse files
authored
ENG-949: Add query sections to left sidebar (#1056)
* ENG-949: Add query sections to left sidebar * ENG-949: Suppress lint warning on new Result-limit setter * ENG-949: Gate query-section detection on {{query block}} marker * Address query sidebar review feedback * Address left sidebar review nits * Clarify query section initial load state * Reuse query block marker for active queries
1 parent a5bb51c commit 616fafa

7 files changed

Lines changed: 473 additions & 17 deletions

File tree

apps/roam/src/components/LeftSidebarView.tsx

Lines changed: 187 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ import {
2929
settingKeys,
3030
} from "~/components/settings/utils/settingsEmitter";
3131
import {
32+
isQueryBlockRef,
3233
type LeftSidebarConfig,
3334
type LeftSidebarPersonalSectionConfig,
3435
mergeGlobalSectionWithAccessor,
3536
mergePersonalSectionsWithAccessor,
3637
} from "~/utils/getLeftSidebarSettings";
38+
import runQuery from "~/utils/runQuery";
3739
import { sectionsToBlockProps } from "./settings/LeftSidebarPersonalSettings";
3840
import discourseConfigRef, { notify } from "~/utils/discourseConfigRef";
3941
import { getLeftSidebarSettings } from "~/utils/getLeftSidebarSettings";
@@ -347,6 +349,169 @@ const PersonalSectionItem = ({
347349
);
348350
};
349351

352+
const QuerySectionItem = ({
353+
section,
354+
sectionIndex,
355+
dragHandle,
356+
onloadArgs,
357+
}: {
358+
section: LeftSidebarPersonalSectionConfig;
359+
sectionIndex: number;
360+
dragHandle: SortableHandle;
361+
onloadArgs: OnloadArgs;
362+
}) => {
363+
const queryUid = extractRef(section.text);
364+
const alias = section.settings?.alias?.value;
365+
const queryLabel = useMemo(() => getTextByBlockUid(queryUid), [queryUid]);
366+
const displayName = alias || queryLabel || section.text;
367+
const truncateAt = section.settings?.truncateResult.value;
368+
const resultLimit = Math.max(
369+
0,
370+
Math.trunc(section.settings?.resultLimit?.value ?? 10),
371+
);
372+
373+
const [isOpen, setIsOpen] = useState<boolean>(
374+
!!section.settings?.folded.value,
375+
);
376+
const [results, setResults] = useState<ChildNode[]>([]);
377+
const [isLoading, setIsLoading] = useState(false);
378+
const [hasCompletedInitialLoad, setHasCompletedInitialLoad] = useState(false);
379+
const [error, setError] = useState<string | null>(null);
380+
const [isMenuOpen, setIsMenuOpen] = useState(false);
381+
const isTogglingRef = useRef(false);
382+
383+
const loadResults = useCallback(async () => {
384+
setIsLoading(true);
385+
setError(null);
386+
try {
387+
const { allProcessedResults } = await runQuery({
388+
parentUid: queryUid,
389+
extensionAPI: onloadArgs.extensionAPI,
390+
});
391+
const children: ChildNode[] = allProcessedResults.map((r) => {
392+
const isPage = !!getPageTitleByPageUid(r.uid);
393+
return {
394+
uid: r.uid,
395+
text: isPage ? r.uid : `((${r.uid}))`,
396+
};
397+
});
398+
setResults(children);
399+
} catch (e) {
400+
console.error(e);
401+
setError("Query failed to run");
402+
} finally {
403+
setIsLoading(false);
404+
setHasCompletedInitialLoad(true);
405+
}
406+
}, [queryUid, onloadArgs.extensionAPI]);
407+
408+
useEffect(() => {
409+
if (isOpen && !hasCompletedInitialLoad) {
410+
void loadResults();
411+
}
412+
}, [isOpen, hasCompletedInitialLoad, loadResults]);
413+
414+
const handleChevronClick = async () => {
415+
if (!section.settings) return;
416+
if (isTogglingRef.current) return;
417+
isTogglingRef.current = true;
418+
try {
419+
await toggleFoldedState({
420+
isOpen,
421+
setIsOpen,
422+
folded: section.settings.folded,
423+
parentUid: section.settings.uid || "",
424+
sectionIndex,
425+
});
426+
} finally {
427+
isTogglingRef.current = false;
428+
}
429+
};
430+
431+
const limitedResults =
432+
resultLimit > 0 ? results.slice(0, resultLimit) : results;
433+
434+
let body: React.ReactNode = null;
435+
if (isLoading) {
436+
body = <div className="pl-8 pr-2.5 text-sm text-gray-500">Loading…</div>;
437+
} else if (error) {
438+
body = <div className="pl-8 pr-2.5 text-sm text-red-500">{error}</div>;
439+
} else if (limitedResults.length > 0) {
440+
body = limitedResults.map((child) => (
441+
<ChildRow
442+
key={child.uid}
443+
child={child}
444+
truncateAt={truncateAt}
445+
onloadArgs={onloadArgs}
446+
/>
447+
));
448+
} else if (hasCompletedInitialLoad) {
449+
body = <div className="pl-8 pr-2.5 text-sm text-gray-500">No results</div>;
450+
}
451+
452+
return (
453+
<>
454+
<div
455+
{...dragHandle.attributes}
456+
{...dragHandle.listeners}
457+
className="sidebar-title-button flex w-full cursor-pointer items-center border-none bg-transparent pl-6 pr-2.5 font-semibold outline-none"
458+
>
459+
<div className="flex w-full items-center justify-between">
460+
<div
461+
className="flex flex-1 items-center"
462+
onClick={() => void handleChevronClick()}
463+
>
464+
{displayName.toUpperCase()}
465+
</div>
466+
<span
467+
className="sidebar-title-button-chevron p-1"
468+
onClick={() => void handleChevronClick()}
469+
>
470+
<Icon icon={isOpen ? "chevron-down" : "chevron-right"} />
471+
</span>
472+
<Popover
473+
interactionKind={PopoverInteractionKind.CLICK}
474+
position={Position.BOTTOM_RIGHT}
475+
autoFocus={false}
476+
enforceFocus={false}
477+
captureDismiss
478+
isOpen={isMenuOpen}
479+
onInteraction={(next) => setIsMenuOpen(next)}
480+
onClose={() => setIsMenuOpen(false)}
481+
popoverClassName="dg-leftsidebar-popover"
482+
minimal
483+
content={
484+
<Menu>
485+
<MenuItem
486+
icon="refresh"
487+
text="Refresh"
488+
onClick={() => {
489+
void loadResults();
490+
setIsMenuOpen(false);
491+
}}
492+
/>
493+
<MenuItem
494+
icon="document-open"
495+
text="Go to query block"
496+
onClick={(e) => {
497+
void openTarget(e, `((${queryUid}))`, onloadArgs);
498+
setIsMenuOpen(false);
499+
}}
500+
/>
501+
</Menu>
502+
}
503+
>
504+
<span className="sidebar-title-button-add p-1">
505+
<Icon icon="more" size={14} />
506+
</span>
507+
</Popover>
508+
</div>
509+
</div>
510+
<Collapse isOpen={isOpen}>{body}</Collapse>
511+
</>
512+
);
513+
};
514+
350515
const PersonalSections = ({
351516
config,
352517
setConfig,
@@ -424,15 +589,28 @@ const PersonalSections = ({
424589
getId={(s) => s.uid}
425590
onReorder={reorderSections}
426591
className="personal-left-sidebar-sections"
427-
renderItem={(section, handle) => (
428-
<PersonalSectionItem
429-
section={section}
430-
sectionIndex={sections.findIndex((s) => s.uid === section.uid)}
431-
dragHandle={handle}
432-
onChildrenReorder={reorderChildren}
433-
onloadArgs={onloadArgs}
434-
/>
435-
)}
592+
renderItem={(section, handle) => {
593+
const sectionIndex = sections.findIndex((s) => s.uid === section.uid);
594+
if (isQueryBlockRef(section.text) && section.settings?.uid) {
595+
return (
596+
<QuerySectionItem
597+
section={section}
598+
sectionIndex={sectionIndex}
599+
dragHandle={handle}
600+
onloadArgs={onloadArgs}
601+
/>
602+
);
603+
}
604+
return (
605+
<PersonalSectionItem
606+
section={section}
607+
sectionIndex={sectionIndex}
608+
dragHandle={handle}
609+
onChildrenReorder={reorderChildren}
610+
onloadArgs={onloadArgs}
611+
/>
612+
);
613+
}}
436614
/>
437615
);
438616
};

0 commit comments

Comments
 (0)