From 4b62486c07c6b39169730ec139cb9f79fb63d143 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 26 Sep 2025 01:19:02 +0530 Subject: [PATCH 1/8] Let user rearrange personal section and section items in place in left sidebar --- apps/roam/package.json | 1 + .../components/DiscourseSuggestionsBody.tsx | 596 + apps/roam/src/components/LeftSidebarView.tsx | 207 +- .../settings/LeftSidebarGlobalSettings.tsx | 93 +- .../settings/LeftSidebarPersonalSettings.tsx | 317 +- apps/roam/src/utils/getLeftSidebarSettings.ts | 14 - package-lock.json | 29641 ++++++++++++++++ 7 files changed, 30749 insertions(+), 120 deletions(-) create mode 100644 apps/roam/src/components/DiscourseSuggestionsBody.tsx create mode 100644 package-lock.json diff --git a/apps/roam/package.json b/apps/roam/package.json index e955ec506..987662b4a 100644 --- a/apps/roam/package.json +++ b/apps/roam/package.json @@ -53,6 +53,7 @@ "@use-gesture/react": "^10.2.27", "@vercel/blob": "^1.1.1", "classnames": "^2.3.2", + "@hello-pangea/dnd": "^18.0.1", "contrast-color": "^1.0.1", "core-js": "^3.45.0", "cytoscape": "^3.21.0", diff --git a/apps/roam/src/components/DiscourseSuggestionsBody.tsx b/apps/roam/src/components/DiscourseSuggestionsBody.tsx new file mode 100644 index 000000000..a8c0eca9f --- /dev/null +++ b/apps/roam/src/components/DiscourseSuggestionsBody.tsx @@ -0,0 +1,596 @@ +import React, { useMemo, useState, useEffect, useCallback } from "react"; +import { + Button, + ControlGroup, + Intent, + Menu, + MenuItem, + Position, + Popover, + Spinner, + Tag, + Tooltip, +} from "@blueprintjs/core"; +import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; +import getAllPageNames from "roamjs-components/queries/getAllPageNames"; +import openBlockInSidebar from "roamjs-components/writes/openBlockInSidebar"; +import { Result } from "roamjs-components/types/query-builder"; +import { useExtensionAPI } from "roamjs-components/components/ExtensionApiContext"; +import { performHydeSearch, type RelationDetails } from "~/utils/hyde"; +import { createBlock } from "roamjs-components/writes"; +import getDiscourseContextResults from "~/utils/getDiscourseContextResults"; +import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; +import findDiscourseNode from "~/utils/findDiscourseNode"; +import getDiscourseRelations from "~/utils/getDiscourseRelations"; +import getDiscourseNodes from "~/utils/getDiscourseNodes"; +import normalizePageTitle from "roamjs-components/queries/normalizePageTitle"; + +export type DiscourseData = { + results: Awaited>; + refs: number; +}; + +const cache: { + [tag: string]: DiscourseData; +} = {}; + +const getOverlayInfo = async ( + tag: string, + relations: ReturnType, +): Promise => { + try { + if (cache[tag]) return cache[tag]; + + const nodes = getDiscourseNodes(relations); + + const [results, refs] = await Promise.all([ + getDiscourseContextResults({ + uid: getPageUidByPageTitle(tag), + nodes, + relations, + }), + // @ts-ignore - backend to be added to roamjs-components + window.roamAlphaAPI.data.backend.q( + `[:find ?a :where [?b :node/title "${normalizePageTitle(tag)}"] [?a :block/refs ?b]]`, + ), + ]); + + return (cache[tag] = { + results, + refs: refs.length, + }); + } catch (error) { + console.error(`Error getting overlay info for ${tag}:`, error); + return { + results: [], + refs: 0, + }; + } +}; + +export const useDiscourseNodeFilters = (tag: string) => { + const [results, setResults] = useState([]); + + const tagUid = useMemo(() => getPageUidByPageTitle(tag), [tag]); + const discourseNode = useMemo(() => findDiscourseNode(tagUid), [tagUid]); + const relations = useMemo(() => getDiscourseRelations(), []); + const allNodes = useMemo(() => getDiscourseNodes(), []); + + const getInfo = useCallback( + () => + getOverlayInfo(tag, relations) + .then(({ results }) => { + setResults(results); + }) + .finally(() => {}), + [tag, relations], + ); + + useEffect(() => { + void getInfo(); + }, [getInfo]); + + const validRelations = useMemo(() => { + if (!discourseNode) return []; + const selfType = discourseNode.type; + + return relations.filter( + (relation) => + relation.source === selfType || relation.destination === selfType, + ); + }, [relations, discourseNode]); + + const uniqueRelationTypeTriplets = useMemo(() => { + if (!discourseNode) return []; + const relatedNodeType = discourseNode.type; + const uniqueTriplets = new Map(); + + validRelations.forEach((relation) => { + const isSelfSource = relation.source === relatedNodeType; + const isSelfDestination = relation.destination === relatedNodeType; + + if (isSelfSource) { + const targetNodeType = relation.destination; + const identifiedTargetNode = allNodes.find( + (node) => node.type === targetNodeType, + ); + + if (identifiedTargetNode) { + const key = `${relation.label}-${identifiedTargetNode.text}`; + uniqueTriplets.set(key, { + relationLabel: relation.label, + relatedNodeText: identifiedTargetNode.text, + relatedNodeFormat: identifiedTargetNode.format, + }); + } + } + + if (isSelfDestination) { + const targetNodeType = relation.source; + const identifiedTargetNode = allNodes.find( + (node) => node.type === targetNodeType, + ); + + if (identifiedTargetNode) { + const key = `${relation.complement}-${identifiedTargetNode.text}`; + uniqueTriplets.set(key, { + relationLabel: relation.complement, + relatedNodeText: identifiedTargetNode.text, + relatedNodeFormat: identifiedTargetNode.format, + }); + } + } + }); + + return Array.from(uniqueTriplets.values()); + }, [validRelations, discourseNode, allNodes]); + + const validTypes = useMemo(() => { + if (!discourseNode) return []; + const selfType = discourseNode.type; + + const hasSelfRelation = validRelations.some( + (relation) => + relation.source === selfType && relation.destination === selfType, + ); + const types = Array.from( + new Set( + validRelations.flatMap((relation) => [ + relation.source, + relation.destination, + ]), + ), + ); + return hasSelfRelation ? types : types.filter((type) => type !== selfType); + }, [discourseNode, validRelations]); + + return { + results, + discourseNode, + allNodes, + uniqueRelationTypeTriplets, + validTypes, + }; +}; + +const MAX_CACHE_SIZE = 50; + +type SuggestedNode = Result & { type: string }; +type SuggestionsCache = { + selectedPages: string[]; + useAllPagesForSuggestions: boolean; + hydeFilteredNodes: SuggestedNode[]; + activeNodeTypeFilters: string[]; +}; +const suggestionsCache = new Map(); + +const SuggestionsBody = ({ + tag, + blockUid, + shouldGrabFromReferencedPages, + shouldGrabParentChildContext, +}: { + tag: string; + blockUid: string; + shouldGrabFromReferencedPages: boolean; + shouldGrabParentChildContext: boolean; +}) => { + const { + results: existingResults, + discourseNode, + uniqueRelationTypeTriplets, + validTypes, + allNodes, + } = useDiscourseNodeFilters(tag); + const allPages = useMemo(() => getAllPageNames(), []); + const cachedState = suggestionsCache.get(blockUid); + const extensionAPI = useExtensionAPI(); + + const [currentPageInput, setCurrentPageInput] = useState(""); + const [selectedPages, setSelectedPages] = useState( + cachedState?.selectedPages || [], + ); + const [useAllPagesForSuggestions, setUseAllPagesForSuggestions] = useState( + cachedState?.useAllPagesForSuggestions || false, + ); + const [autocompleteKey, setAutocompleteKey] = useState(0); + const [pageGroups, setPageGroups] = useState>({}); + const [hydeFilteredNodes, setHydeFilteredNodes] = useState( + cachedState?.hydeFilteredNodes || [], + ); + const [activeNodeTypeFilters, setActiveNodeTypeFilters] = useState( + cachedState?.activeNodeTypeFilters || [], + ); + const [isSearchingHyde, setIsSearchingHyde] = useState(false); + + const actuallyDisplayedNodes = useMemo(() => { + if (activeNodeTypeFilters.length === 0) return hydeFilteredNodes; + return hydeFilteredNodes.filter((n) => + activeNodeTypeFilters.includes(n.type), + ); + }, [hydeFilteredNodes, activeNodeTypeFilters]); + + const uniqueSuggestedTypeUIDs = useMemo( + () => Array.from(new Set(hydeFilteredNodes.map((n) => n.type))), + [hydeFilteredNodes], + ); + + const availableFilterTypes = useMemo(() => { + return uniqueSuggestedTypeUIDs + .map((uid) => { + const nodeDef = allNodes.find((n) => n.type === uid); + return { uid, text: nodeDef ? nodeDef.text : uid }; + }) + .sort((a, b) => a.text.localeCompare(b.text)); + }, [uniqueSuggestedTypeUIDs, allNodes]); + + const handleAddPage = () => { + if (currentPageInput && !selectedPages.includes(currentPageInput)) { + setSelectedPages((prev) => [...prev, currentPageInput]); + setTimeout(() => { + setCurrentPageInput(""); + setAutocompleteKey((prev) => prev + 1); + }, 0); + setUseAllPagesForSuggestions(false); + } + }; + + const handleCreateBlock = async (node: SuggestedNode) => { + await createBlock({ + parentUid: blockUid, + node: { text: `[[${node.text}]]` }, + }); + setHydeFilteredNodes((prev) => prev.filter((n) => n.uid !== node.uid)); + }; + + const toggleOverlayHighlight = (nodeUid: string, on: boolean) => { + document + .querySelectorAll(`[data-dg-block-uid="${nodeUid}"]`) + .forEach((el) => el.classList.toggle("dg-highlight", on)); + }; + + useEffect(() => { + const storedGroups = extensionAPI?.settings.get( + "suggestion-page-groups", + ) as Record | undefined; + if (storedGroups && typeof storedGroups === "object") { + setPageGroups(storedGroups); + } + }, [extensionAPI]); + + useEffect(() => { + const data = suggestionsCache.get(blockUid); + if (!data) return; + suggestionsCache.delete(blockUid); + suggestionsCache.set(blockUid, data); + }, [blockUid]); + + useEffect(() => { + const firstKey = suggestionsCache.keys().next().value as string | undefined; + if ( + !suggestionsCache.has(blockUid) && + suggestionsCache.size >= MAX_CACHE_SIZE && + firstKey + ) { + suggestionsCache.delete(firstKey); + } + + suggestionsCache.set(blockUid, { + selectedPages, + useAllPagesForSuggestions, + hydeFilteredNodes, + activeNodeTypeFilters, + }); + }, [ + blockUid, + selectedPages, + useAllPagesForSuggestions, + hydeFilteredNodes, + activeNodeTypeFilters, + ]); + + return ( +
e.stopPropagation()}> +
+ + +
{ + if (e.key === "Enter" && currentPageInput) { + e.preventDefault(); + e.stopPropagation(); + handleAddPage(); + } + }} + > + +
+ +
+ + {selectedPages.length > 0 && ( +
+ {selectedPages.map((p) => ( + { + setSelectedPages((prev) => prev.filter((x) => x !== p)); + }} + round + minimal + > + {p} + + ))} +
+ )} +
+ + {(hydeFilteredNodes.length > 0 || isSearchingHyde) && ( +
+
+

+ {useAllPagesForSuggestions + ? "From All Pages" + : selectedPages.length > 0 + ? `From ${selectedPages.length === 1 ? `"${selectedPages[0]}"` : `${selectedPages.length} selected pages`}` + : "Select pages to see suggestions"} +

+ + Total nodes: {actuallyDisplayedNodes.length} + + + {availableFilterTypes.map((t) => ( +
+ } + > +
+
+
+ {isSearchingHyde && ( + + )} +
    + {!isSearchingHyde && + actuallyDisplayedNodes.length > 0 && + actuallyDisplayedNodes.map((node) => ( +
  • + toggleOverlayHighlight(node.uid, true) + } + onMouseLeave={() => + toggleOverlayHighlight(node.uid, false) + } + > + { + if (e.shiftKey) { + void openBlockInSidebar(node.uid); + } else { + void window.roamAlphaAPI.ui.mainWindow.openPage({ + page: { uid: node.uid }, + }); + } + }} + > + {node.text} + +
  • + ))} + {!isSearchingHyde && actuallyDisplayedNodes.length === 0 && ( +
  • + {hydeFilteredNodes.length > 0 && + activeNodeTypeFilters.length > 0 + ? "No suggestions match the current filters." + : "No relevant relations found."} +
  • + )} +
+
+
+ + )} + + ); +}; + +export default SuggestionsBody; diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index 8e2628551..d0d46d242 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -7,6 +7,17 @@ import React, { useState, } from "react"; import ReactDOM from "react-dom"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, + DraggableProvided, + DraggableStateSnapshot, + DroppableProvided, + DragStart, + DraggingRubric, +} from "@hello-pangea/dnd"; import { Collapse, Icon, @@ -147,8 +158,10 @@ const SectionChildren = ({ const PersonalSectionItem = ({ section, + activeDragSourceId, }: { section: LeftSidebarPersonalSectionConfig; + activeDragSourceId: string | null; }) => { const titleRef = parseReference(section.text); const blockText = useMemo( @@ -160,7 +173,33 @@ const PersonalSectionItem = ({ const [isOpen, setIsOpen] = useState( !!section.settings?.folded.value || false, ); - const alias = section.settings?.alias?.value; + + const renderChild = ( + dragProvided: DraggableProvided, + child: { text: string; uid: string }, + ) => { + const ref = parseReference(child.text); + const label = truncate(ref.display, truncateAt); + const onClick = (e: React.MouseEvent) => { + return void openTarget(e, child.text); + }; + return ( +
+
+ {label} +
+
+ ); + }; const handleChevronClick = () => { if (!section.settings) return; @@ -187,7 +226,7 @@ const PersonalSectionItem = ({ } }} > - {(alias || blockText || titleRef.display).toUpperCase()} + {(blockText || titleRef.display).toUpperCase()} {(section.children?.length || 0) > 0 && ( - + { + const child = (section.children || [])[rubric.source.index]; + return renderChild(provided, child); + }} + > + {(provided: DroppableProvided) => ( +
+ {(section.children || []).map((child, index) => ( + + {(dragProvided: DraggableProvided) => { + return renderChild(dragProvided, child); + }} + + ))} + {provided.placeholder} +
+ )} +
); @@ -211,17 +279,122 @@ const PersonalSectionItem = ({ const PersonalSections = ({ config, + setConfig, }: { - config: LeftSidebarConfig["personal"]; + config: LeftSidebarConfig; + setConfig: Dispatch>; }) => { - const sections = config.sections || []; + const sections = config.personal.sections || []; + if (!sections.length) return null; + const [activeDragSourceId, setActiveDragSourceId] = useState( + null, + ); + + const handleDragStart = (start: DragStart) => { + if (start.type === "ITEMS") { + setActiveDragSourceId(start.source.droppableId); + } + }; + + const handleDragEnd = (result: DropResult) => { + setActiveDragSourceId(null); + const { source, destination, type } = result; + + if (!destination) return; + + if (type === "SECTIONS") { + if (destination.index === source.index) return; + + const newPersonalSections = Array.from(config.personal.sections); + const [removed] = newPersonalSections.splice(source.index, 1); + newPersonalSections.splice(destination.index, 0, removed); + + setConfig({ + ...config, + personal: { ...config.personal, sections: newPersonalSections }, + }); + const finalIndex = + destination.index > source.index + ? destination.index + 1 + : destination.index; + void window.roamAlphaAPI.moveBlock({ + location: { "parent-uid": config.personal.uid, order: finalIndex }, + block: { uid: removed.uid }, + }); + return; + } + + if (type === "ITEMS") { + if (source.droppableId !== destination.droppableId) { + return; + } + + if (destination.index === source.index) { + return; + } + + const newConfig = JSON.parse(JSON.stringify(config)) as LeftSidebarConfig; + const { personal } = newConfig; + + const listToReorder = personal.sections.find( + (s) => s.uid === source.droppableId, + ); + const parentUid = listToReorder?.childrenUid; + const listToReorderChildren = listToReorder?.children; + + if (!listToReorderChildren) return; + + const [removedItem] = listToReorderChildren.splice(source.index, 1); + listToReorderChildren.splice(destination.index, 0, removedItem); + + setConfig(newConfig); + const finalIndex = + destination.index > source.index + ? destination.index + 1 + : destination.index; + void window.roamAlphaAPI.moveBlock({ + location: { "parent-uid": parentUid || "", order: finalIndex }, + block: { uid: removedItem.uid }, + }); + } + }; return ( -
- {sections.map((s) => ( - - ))} -
+ + + {(provided: DroppableProvided) => ( +
+ {sections.map((section, index) => ( + + {(dragProvided: DraggableProvided) => ( +
+ +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
); }; @@ -400,12 +573,16 @@ const FavouritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { }; const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { - const config = useConfig(); + const initialConfig = useConfig(); + const [config, setConfig] = useState(initialConfig); + useEffect(() => { + setConfig(initialConfig); + }, [initialConfig]); return ( <> - + ); }; diff --git a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx index cebb853ae..b80398be5 100644 --- a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx @@ -13,6 +13,13 @@ import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; import { getLeftSidebarGlobalSectionConfig } from "~/utils/getLeftSidebarSettings"; import { LeftSidebarGlobalSectionConfig } from "~/utils/getLeftSidebarSettings"; import { render as renderToast } from "roamjs-components/components/Toast"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, + DraggableProvided, +} from "@hello-pangea/dnd"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { refreshAndNotify } from "~/components/LeftSidebarView"; @@ -20,12 +27,20 @@ const PageItem = memo( ({ page, onRemove, + dragProvided, }: { page: RoamBasicNode; onRemove: (page: RoamBasicNode) => void; + dragProvided: DraggableProvided; }) => { return ( -
+
{page.text}
{pages.length > 0 ? ( -
- {pages.map((page) => ( - void removePage(page)} - /> - ))} -
+ + { + const page = pages[rubric.source.index]; + return ( + void removePage(page)} + dragProvided={provided} + /> + ); + }} + > + {(provided) => ( +
+ {pages.map((page, index) => ( + + {(dragProvided) => ( + void removePage(page)} + dragProvided={dragProvided} + /> + )} + + ))} + {provided.placeholder} +
+ )} +
+
) : (
No pages added yet diff --git a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx index e4806f1d7..5506de9ea 100644 --- a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx @@ -1,14 +1,12 @@ import discourseConfigRef from "~/utils/discourseConfigRef"; import React, { useCallback, useEffect, useMemo, useState } from "react"; -import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; import getAllPageNames from "roamjs-components/queries/getAllPageNames"; -import { Button, Dialog, Collapse, Tooltip } from "@blueprintjs/core"; +import { Button, Dialog, Collapse, InputGroup, Icon } from "@blueprintjs/core"; import createBlock from "roamjs-components/writes/createBlock"; import deleteBlock from "roamjs-components/writes/deleteBlock"; import type { RoamBasicNode } from "roamjs-components/types"; import NumberPanel from "roamjs-components/components/ConfigPanels/NumberPanel"; -import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; import { LeftSidebarPersonalSectionConfig, getLeftSidebarPersonalSectionConfig, @@ -21,6 +19,16 @@ import { render as renderToast } from "roamjs-components/components/Toast"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { refreshAndNotify } from "~/components/LeftSidebarView"; import { memo, Dispatch, SetStateAction } from "react"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, + DraggableProvided, + DroppableProvided, + DraggingRubric, + DraggableStateSnapshot, +} from "@hello-pangea/dnd"; const SectionItem = memo( ({ @@ -28,16 +36,17 @@ const SectionItem = memo( setSettingsDialogSectionUid, pageNames, setSections, + dragHandleProps, }: { section: LeftSidebarPersonalSectionConfig; setSections: Dispatch>; setSettingsDialogSectionUid: (uid: string | null) => void; pageNames: string[]; + dragHandleProps: DraggableProvided["dragHandleProps"]; }) => { const ref = extractRef(section.text); const blockText = getTextByBlockUid(ref); const originalName = blockText || section.text; - const alias = section.settings?.alias?.value; const [childInput, setChildInput] = useState(""); const [childInputKey, setChildInputKey] = useState(0); @@ -75,11 +84,6 @@ const SectionItem = memo( order: 1, node: { text: "Truncate-result?", children: [{ text: "75" }] }, }); - const aliasUid = await createBlock({ - parentUid: settingsUid, - order: 2, - node: { text: "Alias" }, - }); const childrenUid = await createBlock({ parentUid: section.uid, @@ -96,7 +100,6 @@ const SectionItem = memo( uid: settingsUid, folded: { uid: foldedUid, value: false }, truncateResult: { uid: truncateSettingUid, value: 75 }, - alias: { uid: aliasUid, value: "" }, }, childrenUid, children: [], @@ -222,7 +225,7 @@ const SectionItem = memo( }, [childInput, section, addChildToSection]); const sectionWithoutSettingsAndChildren = - !section.settings && !section.children; + (!section.settings && section.children?.length == 0) || !section.children; return (
+
+ +
+
{!sectionWithoutSettingsAndChildren && (
+ ); + }} + > + {(provided: DroppableProvided) => (
- {child.text} -
+ )} + + ))} + {provided.placeholder}
- ))} -
+ )} + )} {(!section.children || section.children.length === 0) && ( @@ -434,6 +499,77 @@ const LeftSidebarPersonalSectionsContent = ({ [personalSectionUid, sections], ); + const handleDragEnd = useCallback( + (result: DropResult) => { + const { source, destination, type } = result; + if (!destination) return; + + if (type === "SECTIONS") { + if (destination.index === source.index) return; + + const newSections = Array.from(sections); + const [removed] = newSections.splice(source.index, 1); + newSections.splice(destination.index, 0, removed); + setSections(newSections); + + const finalIndex = + destination.index > source.index + ? destination.index + 1 + : destination.index; + void window.roamAlphaAPI + .moveBlock({ + location: { "parent-uid": personalSectionUid!, order: finalIndex }, + block: { uid: removed.uid }, + }) + .then(() => { + refreshAndNotify(); + }); + return; + } + + if (type === "ITEMS") { + if (source.droppableId !== destination.droppableId) { + return; + } + if (destination.index === source.index) return; + + const sectionToReorder = sections.find( + (s) => s.uid === source.droppableId, + ); + if (!sectionToReorder || !sectionToReorder.children) return; + + const newChildren = Array.from(sectionToReorder.children); + const [removed] = newChildren.splice(source.index, 1); + newChildren.splice(destination.index, 0, removed); + + const newSections = sections.map((s) => { + if (s.uid === source.droppableId) { + return { ...s, children: newChildren }; + } + return s; + }); + setSections(newSections); + + const finalIndex = + destination.index > source.index + ? destination.index + 1 + : destination.index; + void window.roamAlphaAPI + .moveBlock({ + location: { + "parent-uid": sectionToReorder.childrenUid!, + order: finalIndex, + }, + block: { uid: removed.uid }, + }) + .then(() => { + refreshAndNotify(); + }); + } + }, + [sections, personalSectionUid, setSections], + ); + const handleNewSectionInputChange = useCallback((value: string) => { setNewSectionInput(value); }, []); @@ -464,13 +600,11 @@ const LeftSidebarPersonalSectionsContent = ({ } }} > - handleNewSectionInputChange(e.target.value)} + placeholder="Add section …" />