Skip to content
51 changes: 51 additions & 0 deletions apps/roam/src/components/LeftSidebarCommands.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from "react";
import { Popover, Position, Button, Menu, MenuItem } from "@blueprintjs/core";
import { OnloadArgs } from "roamjs-components/types";
import { createDiscourseNodeFromCommand } from "~/utils/registerCommandPaletteCommands";

export const cleanCommandName = (name: string): string => {
if (name.startsWith("{") && name.endsWith("}"))
name = name.substring(1, name.length - 1);
name = name.trim();
// sentence case
name = name.charAt(0).toUpperCase() + name.slice(1);
return name;
};
Comment thread
maparent marked this conversation as resolved.

export const commands: Record<
string,
(onloadArgs: OnloadArgs) => Promise<void>
> = {
/* eslint-disable @typescript-eslint/require-await */
// eslint-disable-next-line @typescript-eslint/naming-convention
"{create node}": async (onloadArgs: OnloadArgs) => {
createDiscourseNodeFromCommand(onloadArgs.extensionAPI);
// typescript-eslint/naming-convention
},
/* eslint-enable @typescript-eslint/require-await */
};

export const SidebarCommandPopover = ({
onSelect,
}: {
onSelect: (value: string) => void;
}) => {
return (
<Popover
content={
<Menu>
{Object.keys(commands).map((commandName) => (
<MenuItem
key={commandName}
text={commandName}
onClick={() => onSelect(commandName)}
/>
))}
</Menu>
}
position={Position.BOTTOM_LEFT}
>
<Button icon="cog" small minimal title="Commands" />
</Popover>
);
};
89 changes: 67 additions & 22 deletions apps/roam/src/components/LeftSidebarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, {
} from "react";
import ReactDOM from "react-dom";
import {
Button,
Collapse,
Icon,
Popover,
Expand Down Expand Up @@ -43,10 +44,13 @@ import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage";
import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid";
import { migrateLeftSidebarSettings } from "~/utils/migrateLeftSidebarSettings";
import posthog from "posthog-js";
import { commands, cleanCommandName } from "~/components/LeftSidebarCommands";

const parseReference = (text: string) => {
const extracted = extractRef(text);
if (text.startsWith("((") && text.endsWith("))")) {
if (commands[text]) {
return { type: "command" as const, uid: text, display: text };
} else if (text.startsWith("((") && text.endsWith("))")) {
return { type: "block" as const, uid: extracted, display: text };
} else {
return { type: "page" as const, display: text };
Expand All @@ -58,14 +62,22 @@ const truncate = (s: string, max: number | undefined): string => {
return s.length > max ? `${s.slice(0, max)}...` : s;
};

const openTarget = async (e: React.MouseEvent, targetUid: string) => {
const openTarget = async (
e: React.MouseEvent,
targetUid: string,
onloadArgs: OnloadArgs,
) => {
e.preventDefault();
e.stopPropagation();
const target = parseReference(targetUid);
posthog.capture("Left Sidebar: Target Opened", {
targetType: target.type,
openInSidebar: e.shiftKey,
});
if (target.type === "command") {
await commands[target.uid](onloadArgs);
return;
}
if (target.type === "block") {
if (e.shiftKey) {
await openBlockInSidebar(target.uid);
Expand Down Expand Up @@ -123,9 +135,11 @@ const toggleFoldedState = ({
const SectionChildren = ({
childrenNodes,
truncateAt,
onloadArgs,
}: {
childrenNodes: { uid: string; text: string; alias?: { value: string } }[];
truncateAt?: number;
onloadArgs: OnloadArgs;
}) => {
if (!childrenNodes?.length) return null;
return (
Expand All @@ -134,23 +148,33 @@ const SectionChildren = ({
const ref = parseReference(child.text);
const alias = child.alias?.value;
const display =
ref.type === "page"
? getPageTitleByPageUid(ref.display)
: getTextByBlockUid(ref.uid);
ref.type === "command"
? ref.display
: ref.type === "page"
? getPageTitleByPageUid(ref.display)
: getTextByBlockUid(ref.uid);
const label = alias || truncate(display, truncateAt);
const onClick = (e: React.MouseEvent) => {
return void openTarget(e, child.text);
return void openTarget(e, child.text, onloadArgs);
};
return (
<div key={child.uid} className="pl-8 pr-2.5">
<div
className={
"section-child-item page cursor-pointer rounded-sm leading-normal text-gray-600"
}
onClick={onClick}
>
{label}
</div>
{ref.type === "command" ? (
<span className="bp3-dark">
<Button onClick={onClick} minimal className="m-px">
{cleanCommandName(label)}
</Button>
</span>
) : (
<div
className={
"section-child-item page cursor-pointer rounded-sm leading-normal text-gray-600"
}
onClick={onClick}
>
{label}
</div>
)}
</div>
);
})}
Expand All @@ -160,8 +184,10 @@ const SectionChildren = ({

const PersonalSectionItem = ({
section,
onloadArgs,
}: {
section: LeftSidebarPersonalSectionConfig;
onloadArgs: OnloadArgs;
}) => {
const titleRef = parseReference(section.text);
const blockText = useMemo(
Expand Down Expand Up @@ -213,13 +239,20 @@ const PersonalSectionItem = ({
<SectionChildren
childrenNodes={section.children || []}
truncateAt={truncateAt}
onloadArgs={onloadArgs}
/>
</Collapse>
</>
);
};

const PersonalSections = ({ config }: { config: LeftSidebarConfig }) => {
const PersonalSections = ({
config,
onloadArgs,
}: {
config: LeftSidebarConfig;
onloadArgs: OnloadArgs;
}) => {
const sections = config.personal.sections || [];

if (!sections.length) return null;
Expand All @@ -228,14 +261,20 @@ const PersonalSections = ({ config }: { config: LeftSidebarConfig }) => {
<div className="personal-left-sidebar-sections">
{sections.map((section) => (
<div key={section.uid}>
<PersonalSectionItem section={section} />
<PersonalSectionItem section={section} onloadArgs={onloadArgs} />
</div>
))}
</div>
);
};

const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => {
const GlobalSection = ({
config,
onloadArgs,
}: {
config: LeftSidebarConfig["global"];
onloadArgs: OnloadArgs;
}) => {
const [isOpen, setIsOpen] = useState<boolean>(
!!config.settings?.folded.value,
);
Expand Down Expand Up @@ -267,10 +306,16 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => {
</div>
{isCollapsable ? (
<Collapse isOpen={isOpen}>
<SectionChildren childrenNodes={config.children} />
<SectionChildren
childrenNodes={config.children}
onloadArgs={onloadArgs}
/>
</Collapse>
) : (
<SectionChildren childrenNodes={config.children} />
<SectionChildren
childrenNodes={config.children}
onloadArgs={onloadArgs}
/>
)}
</>
);
Expand Down Expand Up @@ -413,8 +458,8 @@ const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
return (
<>
<FavoritesPopover onloadArgs={onloadArgs} />
<GlobalSection config={config.global} />
<PersonalSections config={config} />
<GlobalSection config={config.global} onloadArgs={onloadArgs} />
<PersonalSections config={config} onloadArgs={onloadArgs} />
</>
);
};
Expand Down Expand Up @@ -444,7 +489,7 @@ const migrateFavorites = async () => {
}

const results = window.roamAlphaAPI.q(`
[:find ?uid
[:find ?uid
:where [?e :page/sidebar]
[?e :block/uid ?uid]]
`);
Expand Down
36 changes: 28 additions & 8 deletions apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { refreshAndNotify } from "~/components/LeftSidebarView";
import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid";
import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid";
import posthog from "posthog-js";
import {
commands,
SidebarCommandPopover,
} from "~/components/LeftSidebarCommands";

const pagesToUids = (pages: RoamBasicNode[]) => pages.map((p) => p.text);

Expand Down Expand Up @@ -92,7 +96,11 @@ const LeftSidebarGlobalSectionsContent = ({
const [isInitializing, setIsInitializing] = useState(true);
const [isExpanded, setIsExpanded] = useState(true);

const pageNames = useMemo(() => getAllPageNames(), []);
const commandNames = useMemo(() => Object.keys(commands), []);
const pageAndCommandNames = useMemo(
() => [...commandNames, ...getAllPageNames()],
[commandNames],
);

useEffect(() => {
const initialize = async () => {
Expand Down Expand Up @@ -182,11 +190,21 @@ const LeftSidebarGlobalSectionsContent = ({
[pages, childrenUid],
);

const resetAutocomplete = useCallback((nextValue = "") => {
setNewPageInput(nextValue);

// AutocompleteInput renders from its internal `query` state, which is only
// initialized from the external `value` prop on mount. Bump the key to remount
// it so the displayed input reflects the new parent state.
setAutocompleteKey((prev) => prev + 1);
}, []);

const addPage = useCallback(
async (pageName: string) => {
if (!pageName || !childrenUid) return;

const targetUid = getPageUidByPageTitle(pageName);
const targetUid = commands[pageName]
? pageName
: getPageUidByPageTitle(pageName);
if (pages.some((p) => p.text === targetUid)) {
console.warn(`Page "${pageName}" already exists in global section`);
return;
Expand All @@ -212,8 +230,7 @@ const LeftSidebarGlobalSectionsContent = ({
pagesToUids(updatedPages),
);

setNewPageInput("");
setAutocompleteKey((prev) => prev + 1);
resetAutocomplete("");
posthog.capture("Left Sidebar Global Settings: Page Added", {
pageName,
});
Expand All @@ -226,7 +243,7 @@ const LeftSidebarGlobalSectionsContent = ({
});
}
},
[childrenUid, pages],
[childrenUid, pages, resetAutocomplete],
);

const removePage = useCallback(
Expand Down Expand Up @@ -262,7 +279,9 @@ const LeftSidebarGlobalSectionsContent = ({

const isAddButtonDisabled = useMemo(() => {
if (!newPageInput) return true;
const targetUid = getPageUidByPageTitle(newPageInput);
const targetUid = commands[newPageInput]
? newPageInput
: getPageUidByPageTitle(newPageInput);
return !targetUid || pages.some((p) => p.text === targetUid);
}, [newPageInput, pages]);

Expand Down Expand Up @@ -335,7 +354,7 @@ const LeftSidebarGlobalSectionsContent = ({
value={newPageInput}
setValue={handlePageInputChange}
placeholder="Add page…"
options={pageNames}
options={pageAndCommandNames}
maxItemsDisplayed={50}
autoFocus
onConfirm={() => void addPage(newPageInput)}
Expand All @@ -348,6 +367,7 @@ const LeftSidebarGlobalSectionsContent = ({
onClick={() => void addPage(newPageInput)}
title="Add page"
/>
<SidebarCommandPopover onSelect={resetAutocomplete} />
</div>
{pages.length > 0 ? (
<div className="space-y-1">
Expand Down
Loading
Loading