Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 65 additions & 38 deletions apps/roam/src/components/DiscourseNodeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,25 @@ type Props = {
textarea: HTMLTextAreaElement;
extensionAPI: OnloadArgs["extensionAPI"];
trigger?: JSX.Element;
isShift?: boolean;
isTextSelected?: boolean;
};

const NodeMenu = ({
onClose,
textarea,
extensionAPI,
trigger,
isShift,
isTextSelected,
}: { onClose: () => void } & Props) => {
const [showNodeTypes, setShowNodeTypes] = useState(isTextSelected || isShift);
const discourseNodes = useMemo(
() => getDiscourseNodes().filter((n) => n.backedBy === "user"),
[],
() =>
getDiscourseNodes().filter(
(n) => n.backedBy === "user" && (showNodeTypes || n.tag),
),
[showNodeTypes],
);
const indexBySC = useMemo(
() => Object.fromEntries(discourseNodes.map((mi, i) => [mi.shortcut, i])),
Expand All @@ -56,51 +64,64 @@ const NodeMenu = ({

const onSelect = useCallback(
(index) => {
const menuItem =
menuRef.current?.children[index].querySelector(".bp3-menu-item");
if (!menuItem) return;
const nodeUid = menuItem.getAttribute("data-node") || "";
const highlighted = textarea.value.substring(
textarea.selectionStart,
textarea.selectionEnd,
);
setTimeout(async () => {
const pageName = await getNewDiscourseNodeText({
text: highlighted,
nodeType: nodeUid,
blockUid,
});
if (showNodeTypes) {
const menuItem =
menuRef.current?.children[index].querySelector(".bp3-menu-item");
if (!menuItem) return;
const nodeUid = menuItem.getAttribute("data-node") || "";
const highlighted = textarea.value.substring(
textarea.selectionStart,
textarea.selectionEnd,
);
setTimeout(async () => {
const pageName = await getNewDiscourseNodeText({
text: highlighted,
nodeType: nodeUid,
blockUid,
});

if (!pageName) {
return;
}
if (!pageName) {
return;
}

const currentBlockText = getTextByBlockUid(blockUid);
const newText = `${currentBlockText.substring(
0,
textarea.selectionStart,
)}[[${pageName}]]${currentBlockText.substring(textarea.selectionEnd)}`;
const currentBlockText = getTextByBlockUid(blockUid);
const newText = `${currentBlockText.substring(
0,
textarea.selectionStart,
)}[[${pageName}]]${currentBlockText.substring(textarea.selectionEnd)}`;

updateBlock({ text: newText, uid: blockUid });
posthog.capture("Discourse Node: Created via Node Menu", {
nodeType: nodeUid,
text: pageName,
});
updateBlock({ text: newText, uid: blockUid });
posthog.capture("Discourse Node: Created via Node Menu", {
nodeType: nodeUid,
text: pageName,
});

createDiscourseNode({
text: pageName,
configPageUid: nodeUid,
extensionAPI,
createDiscourseNode({
text: pageName,
configPageUid: nodeUid,
extensionAPI,
});
});
});
} else {
const menuItem =
menuRef.current?.children[index].querySelector(".bp3-menu-item");
if (!menuItem) return;
const tag = menuItem.getAttribute("data-tag") || "";
console.log(`Tag selected: ${tag}`);
}
onClose();
},
[menuRef, blockUid, onClose, textarea, extensionAPI],
[menuRef, blockUid, onClose, textarea, extensionAPI, showNodeTypes],
);

const keydownListener = useCallback(
(e: KeyboardEvent) => {
if (e.metaKey || e.ctrlKey || e.shiftKey) return;
if (e.metaKey || e.ctrlKey) return;
if (e.key === "Shift") {
setShowNodeTypes((s) => !s);
return;
}
if (e.shiftKey) return;

if (e.key === "ArrowDown") {
const index = Number(
Expand Down Expand Up @@ -190,7 +211,8 @@ const NodeMenu = ({
<MenuItem
key={item.text}
data-node={item.type}
text={item.text}
data-tag={item.tag}
text={showNodeTypes ? item.text : `#${item.tag}`}
active={i === activeIndex}
onMouseEnter={() => setActiveIndex(i)}
onClick={() => onSelect(i)}
Expand All @@ -204,7 +226,9 @@ const NodeMenu = ({
/>
}
labelElement={
<span className="font-mono">{item.shortcut}</span>
<span className="font-mono">
{showNodeTypes ? item.shortcut : ""}
</span>
}
/>
);
Expand Down Expand Up @@ -238,10 +262,12 @@ export const TextSelectionNodeMenu = ({
textarea,
extensionAPI,
onClose,
isTextSelected,
}: {
textarea: HTMLTextAreaElement;
extensionAPI: OnloadArgs["extensionAPI"];
onClose: () => void;
isTextSelected?: boolean;
}) => {
const trigger = (
<Button
Expand Down Expand Up @@ -275,6 +301,7 @@ export const TextSelectionNodeMenu = ({
extensionAPI={extensionAPI}
trigger={trigger}
onClose={onClose}
isTextSelected={isTextSelected}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ const DiscourseNodeConfigPanel: React.FC<DiscourseNodeConfigPanelProps> = ({
text: "Shortcut",
children: [{ text: label.slice(0, 1).toUpperCase() }],
},
{
text: "Tag",
children: [{ text: "" }],
},
{
text: "Format",
children: [
Expand All @@ -96,6 +100,7 @@ const DiscourseNodeConfigPanel: React.FC<DiscourseNodeConfigPanelProps> = ({
type: valueUid,
text: label,
shortcut: "",
tag: "",
specification: [],
backedBy: "user",
canvasSettings: {},
Expand Down
83 changes: 73 additions & 10 deletions apps/roam/src/components/settings/NodeConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useCallback } from "react";
import { DiscourseNode } from "~/utils/getDiscourseNodes";
import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel";
import SelectPanel from "roamjs-components/components/ConfigPanels/SelectPanel";
Expand All @@ -12,6 +12,7 @@ import DiscourseNodeAttributes from "./DiscourseNodeAttributes";
import DiscourseNodeCanvasSettings from "./DiscourseNodeCanvasSettings";
import DiscourseNodeIndex from "./DiscourseNodeIndex";
import { OnloadArgs } from "roamjs-components/types";
import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid";

const NodeConfig = ({
node,
Expand All @@ -28,6 +29,7 @@ const NodeConfig = ({
const formatUid = getUid("Format");
const descriptionUid = getUid("Description");
const shortcutUid = getUid("Shortcut");
const tagUid = getUid("Tag");
const templateUid = getUid("Template");
const overlayUid = getUid("Overlay");
const canvasUid = getUid("Canvas");
Expand All @@ -40,6 +42,45 @@ const NodeConfig = ({
});

const [selectedTabId, setSelectedTabId] = useState<TabId>("general");
const [tagError, setTagError] = useState("");
const [formatError, setFormatError] = useState("");

const getCleanTagText = (tag: string): string => {
return tag.replace(/^#+/, "").trim().toUpperCase();
};

const validateOnBlur = useCallback(() => {
const tagValue = getBasicTreeByParentUid(tagUid)[0]?.text || "";
const formatValue = getBasicTreeByParentUid(formatUid)[0]?.text || "";

const cleanTag = getCleanTagText(tagValue);

if (!cleanTag) {
setTagError("");
setFormatError("");
return;
}

const formatWithoutPlaceholders = formatValue.replace(/{[^}]+}/g, "");
const formatUpper = formatWithoutPlaceholders.toUpperCase();
const formatParts = formatUpper.split(/[^A-Z0-9]/);
const hasConflict = formatParts.includes(cleanTag);

if (hasConflict) {
const formatForMessage = formatWithoutPlaceholders
.trim()
.replace(/(\s*-)*$/, "");
setFormatError(
`Format "${formatForMessage}" conflicts with tag: "${tagValue}". Please use some other format.`,
);
setTagError(
`Tag "${tagValue}" conflicts with format "${formatForMessage}". Please use some other tag.`,
);
} else {
setTagError("");
setFormatError("");
}
}, [tagUid, formatUid]);

return (
<>
Expand All @@ -52,7 +93,7 @@ const NodeConfig = ({
id="general"
title="General"
panel={
<div className="flex flex-col gap-4 p-1">
<div className="flex flex-row gap-4 p-1">
<TextPanel
title="Description"
description={`Describing what the ${node.text} node represents in your graph.`}
Expand All @@ -69,6 +110,21 @@ const NodeConfig = ({
uid={shortcutUid}
defaultValue={node.shortcut}
/>
<div onBlur={validateOnBlur}>
<TextPanel
title="Tag"
description={`Designate a hashtag for marking potential ${node.text}.`}
order={0}
parentUid={node.type}
uid={tagUid}
defaultValue={node.tag}
/>
{tagError && (
<div className="mt-1 text-sm font-medium text-red-600">
{tagError}
</div>
)}
</div>
</div>
}
/>
Expand All @@ -90,14 +146,21 @@ const NodeConfig = ({
title="Format"
panel={
<div className="flex flex-col gap-4 p-1">
<TextPanel
title="Format"
description={`DEPRECATED - Use specification instead. The format ${node.text} pages should have.`}
order={0}
parentUid={node.type}
uid={formatUid}
defaultValue={node.format}
/>
<div onBlur={validateOnBlur}>
<TextPanel
title="Format"
description={`DEPRECATED - Use specification instead. The format ${node.text} pages should have.`}
order={0}
parentUid={node.type}
uid={formatUid}
defaultValue={node.format}
/>
{formatError && (
<div className="mt-1 text-sm font-medium text-red-600">
{formatError}
</div>
)}
</div>
<Label>
Specification
<Description
Expand Down
5 changes: 5 additions & 0 deletions apps/roam/src/utils/getDiscourseNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type DiscourseNode = {
text: string;
type: string;
shortcut: string;
tag: string;
specification: Condition[];
backedBy: "user" | "default" | "relation";
canvasSettings: {
Expand All @@ -32,6 +33,7 @@ const DEFAULT_NODES: DiscourseNode[] = [
text: "Page",
type: "page-node",
shortcut: "p",
tag: "",
format: "{content}",
specification: [
{
Expand All @@ -49,6 +51,7 @@ const DEFAULT_NODES: DiscourseNode[] = [
text: "Block",
type: "blck-node",
shortcut: "b",
tag: "",
format: "{content}",
specification: [
{
Expand Down Expand Up @@ -85,6 +88,7 @@ const getDiscourseNodes = (relations = getDiscourseRelations()) => {
format: getSettingValueFromTree({ tree: children, key: "format" }),
text,
shortcut: getSettingValueFromTree({ tree: children, key: "shortcut" }),
tag: getSettingValueFromTree({ tree: children, key: "tag" }),
type,
specification: getSpecification(children),
backedBy: "user",
Expand All @@ -110,6 +114,7 @@ const getDiscourseNodes = (relations = getDiscourseRelations()) => {
text: r.label,
type: r.id,
shortcut: r.label.slice(0, 1),
tag: "",
specification: r.triples.map(([source, relation, target]) => ({
type: "clause",
source: /anchor/i.test(source) ? r.label : source,
Expand Down
1 change: 1 addition & 0 deletions apps/roam/src/utils/initializeDiscourseNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const initializeDiscourseNodes = async () => {
tree: [
{ text: "Format", children: [{ text: n.format || "" }] },
{ text: "Shortcut", children: [{ text: n.shortcut || "" }] },
{ text: "Tag", children: [{ text: n.tag || "" }] },
{ text: "Graph Overview" },
{
text: "Canvas",
Expand Down
6 changes: 5 additions & 1 deletion apps/roam/src/utils/initializeObserversAndListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,14 @@ export const initObservers = async ({
target.tagName === "TEXTAREA" &&
target.classList.contains("rm-block-input")
) {
const textarea = target as HTMLTextAreaElement;
const isTextSelected = textarea.selectionStart !== textarea.selectionEnd;
removeTextSelectionPopup();
renderDiscourseNodeMenu({
textarea: target as HTMLTextAreaElement,
textarea,
extensionAPI: onloadArgs.extensionAPI,
isShift: evt.shiftKey,
isTextSelected,
});
evt.preventDefault();
evt.stopPropagation();
Expand Down
7 changes: 7 additions & 0 deletions apps/roam/src/utils/renderNodeConfigPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ export const renderNodeConfigPage = ({
// @ts-ignore
Panel: TextPanel,
},
{
title: "Tag",
description: `Designate a hashtag for marking potential ${nodeText}.`,
defaultValue: "",
// @ts-ignore
Panel: TextPanel,
},
{
title: "Description",
description: `Describing what the ${nodeText} node represents in your graph.`,
Expand Down
Loading