diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index f8b657984..d5dbcb1c2 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -27,6 +27,10 @@ import { getNewDiscourseNodeText } from "~/utils/formatUtils"; import { OnloadArgs } from "roamjs-components/types"; import { formatHexColor } from "./settings/DiscourseNodeCanvasSettings"; import posthog from "posthog-js"; +import { + getPersonalSetting, + setPersonalSetting, +} from "./settings/utils/accessors"; type Props = { textarea?: HTMLTextAreaElement; @@ -395,7 +399,9 @@ const normalizeKeyCombo = (combo: string) => { }); }; -export const getModifiersFromCombo = (comboKey: IKeyCombo) => { +export const getModifiersFromCombo = ( + comboKey: IKeyCombo | { key: string; modifiers: number } | undefined, +) => { if (!comboKey) return []; return [ comboKey.modifiers & MODIFIER_BIT_MASKS.alt && "alt", @@ -405,32 +411,26 @@ export const getModifiersFromCombo = (comboKey: IKeyCombo) => { ].filter(Boolean); }; -export const NodeMenuTriggerComponent = ({ - extensionAPI, -}: { - extensionAPI: OnloadArgs["extensionAPI"]; -}) => { +export const NodeMenuTriggerComponent = () => { const inputRef = useRef(null); const [isActive, setIsActive] = useState(false); - const [comboKey, setComboKey] = useState( - () => - (extensionAPI.settings.get( - "personal-node-menu-trigger", - ) as IKeyCombo) || { modifiers: 0, key: "" }, - ); + const [comboKey, setComboKey] = useState(() => { + const saved = getPersonalSetting<{ key: string; modifiers: number }>([ + "Personal Node Menu Trigger", + ]); + return saved ?? { modifiers: 0, key: "" }; + }); - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - e.stopPropagation(); - e.preventDefault(); - const comboObj = getKeyCombo(e.nativeEvent); - if (!comboObj.key) return; + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + e.stopPropagation(); + e.preventDefault(); + const comboObj = getKeyCombo(e.nativeEvent); + if (!comboObj.key) return; - setComboKey({ key: comboObj.key, modifiers: comboObj.modifiers }); - extensionAPI.settings.set("personal-node-menu-trigger", comboObj); - }, - [extensionAPI], - ); + const newCombo = { key: comboObj.key, modifiers: comboObj.modifiers }; + setComboKey(newCombo); + setPersonalSetting(["Personal Node Menu Trigger"], newCombo); + }, []); const shortcut = useMemo(() => { if (!comboKey.key) return ""; @@ -453,8 +453,9 @@ export const NodeMenuTriggerComponent = ({ hidden={!comboKey.key} icon={"remove"} onClick={() => { - setComboKey({ modifiers: 0, key: "" }); - extensionAPI.settings.set("personal-node-menu-trigger", ""); + const emptyCombo = { modifiers: 0, key: "" }; + setComboKey(emptyCombo); + setPersonalSetting(["Personal Node Menu Trigger"], emptyCombo); }} minimal /> diff --git a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx index 6d3656bea..389a33c39 100644 --- a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx @@ -25,6 +25,10 @@ import getDiscourseNodes, { DiscourseNode } from "~/utils/getDiscourseNodes"; import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression"; import { Result } from "~/utils/types"; import { getSetting } from "~/utils/extensionSettings"; +import { + getPersonalSetting, + setPersonalSetting, +} from "~/components/settings/utils/accessors"; import fuzzy from "fuzzy"; type Props = { @@ -605,14 +609,9 @@ export const renderDiscourseNodeSearchMenu = (props: Props) => { ); }; -export const NodeSearchMenuTriggerSetting = ({ - onloadArgs, -}: { - onloadArgs: OnloadArgs; -}) => { - const extensionAPI = onloadArgs.extensionAPI; - const [nodeSearchTrigger, setNodeSearchTrigger] = useState( - getSetting("node-search-trigger", "@"), +export const NodeSearchMenuTriggerSetting = () => { + const [nodeSearchTrigger, setNodeSearchTriggerState] = useState( + () => getPersonalSetting(["Node Search Menu Trigger"])!, ); const handleNodeSearchTriggerChange = ( @@ -625,8 +624,8 @@ export const NodeSearchMenuTriggerSetting = ({ .replace(/\+/g, "\\+") .trim(); - setNodeSearchTrigger(trigger); - extensionAPI.settings.set("node-search-trigger", trigger); + setNodeSearchTriggerState(trigger); + setPersonalSetting(["Node Search Menu Trigger"], trigger); }; return ( ({ tools: (editor, tools) => { // Get the custom keyboard shortcut for the discourse tool - const discourseToolCombo = getSetting(DISCOURSE_TOOL_SHORTCUT_KEY, { - key: "", - modifiers: 0, - }) as IKeyCombo; - - // For discourse tool, just use the key directly since we don't allow modifiers - const discourseToolShortcut = discourseToolCombo?.key?.toUpperCase() || ""; + const discourseToolShortcut = + getPersonalSetting(["Discourse Tool Shortcut"])?.toUpperCase() || + ""; tools["discourse-tool"] = { id: "discourse-tool", diff --git a/apps/roam/src/components/index.ts b/apps/roam/src/components/index.ts index f15c0fb9f..041b6e1c6 100644 --- a/apps/roam/src/components/index.ts +++ b/apps/roam/src/components/index.ts @@ -1,7 +1,7 @@ export { default as DefaultFilters } from "./settings/DefaultFilters"; export { default as DiscourseContext } from "./DiscourseContext"; export { default as DiscourseContextOverlay } from "./DiscourseContextOverlay"; -export { default as DiscourseNodeAttributes } from "./settings/DiscourseNodeAttributes"; +export { default as DiscourseNodeAttributes, DiscourseNodeAttributesTab } from "./settings/DiscourseNodeAttributes"; export { default as DiscourseNodeCanvasSettings } from "./settings/DiscourseNodeCanvasSettings"; export { default as DiscourseNodeIndex } from "./settings/DiscourseNodeIndex"; export { default as DiscourseNodeMenu } from "./DiscourseNodeMenu"; diff --git a/apps/roam/src/components/settings/DiscourseNodeAttributes.tsx b/apps/roam/src/components/settings/DiscourseNodeAttributes.tsx index 49cbc8b90..682b47306 100644 --- a/apps/roam/src/components/settings/DiscourseNodeAttributes.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeAttributes.tsx @@ -1,24 +1,22 @@ -import { Button, InputGroup, Label } from "@blueprintjs/core"; +import { Button, InputGroup, Label, HTMLSelect } from "@blueprintjs/core"; import React, { useRef, useState } from "react"; -import createBlock from "roamjs-components/writes/createBlock"; -import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; -import getFirstChildUidByBlockUid from "roamjs-components/queries/getFirstChildUidByBlockUid"; -import updateBlock from "roamjs-components/writes/updateBlock"; -import deleteBlock from "roamjs-components/writes/deleteBlock"; - -type Attribute = { - uid: string; +import Description from "roamjs-components/components/Description"; +import { + getDiscourseNodeSetting, + setDiscourseNodeSetting, +} from "./utils/accessors"; + +type AttributeEntry = { label: string; value: string; }; const NodeAttribute = ({ - uid, label, value, onChange, onDelete, -}: Attribute & { onChange: (v: string) => void; onDelete: () => void }) => { +}: AttributeEntry & { onChange: (v: string) => void; onDelete: () => void }) => { const timeoutRef = useRef(0); return (
{ clearTimeout(timeoutRef.current); - onChange(e.target.value); + const newValue = e.target.value; + onChange(newValue); timeoutRef.current = window.setTimeout(() => { - updateBlock({ - text: e.target.value, - uid: getFirstChildUidByBlockUid(uid), - }); + // onChange already updates the parent state which saves }, 500); }} /> @@ -53,34 +49,60 @@ const NodeAttribute = ({ ); }; -const NodeAttributes = ({ uid }: { uid: string }) => { - const [attributes, setAttributes] = useState(() => - getBasicTreeByParentUid(uid).map((t) => ({ - uid: t.uid, - label: t.text, - value: t.children[0]?.text, - })), - ); +const NodeAttributes = ({ nodeType }: { nodeType: string }) => { + const [attributes, setAttributes] = useState(() => { + const record = + getDiscourseNodeSetting>(nodeType, [ + "attributes", + ]) ?? {}; + return Object.entries(record).map(([label, value]) => ({ label, value })); + }); const [newAttribute, setNewAttribute] = useState(""); + + const saveAttributes = (newAttributes: AttributeEntry[]) => { + const record: Record = {}; + for (const attr of newAttributes) { + record[attr.label] = attr.value; + } + setDiscourseNodeSetting(nodeType, ["attributes"], record); + }; + + const handleChange = (label: string, newValue: string) => { + const newAttributes = attributes.map((a) => + a.label === label ? { ...a, value: newValue } : a, + ); + setAttributes(newAttributes); + saveAttributes(newAttributes); + }; + + const handleDelete = (label: string) => { + const newAttributes = attributes.filter((a) => a.label !== label); + setAttributes(newAttributes); + saveAttributes(newAttributes); + }; + + const handleAdd = () => { + if (!newAttribute.trim()) return; + const DEFAULT = "{count:Has Any Relation To:any}"; + const newAttributes = [ + ...attributes, + { label: newAttribute.trim(), value: DEFAULT }, + ]; + setAttributes(newAttributes); + saveAttributes(newAttributes); + setNewAttribute(""); + }; + return (
{attributes.map((a) => ( - setAttributes( - attributes.map((aa) => - a.uid === aa.uid ? { ...a, value: v } : aa, - ), - ) - } - onDelete={() => - deleteBlock(a.uid).then(() => - setAttributes(attributes.filter((aa) => a.uid !== aa.uid)), - ) - } + key={a.label} + label={a.label} + value={a.value} + onChange={(v) => handleChange(a.label, v)} + onDelete={() => handleDelete(a.label)} /> ))}
@@ -90,28 +112,18 @@ const NodeAttributes = ({ uid }: { uid: string }) => { setNewAttribute(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleAdd(); + } + }} />
@@ -119,4 +131,118 @@ const NodeAttributes = ({ uid }: { uid: string }) => { ); }; +export const DiscourseNodeAttributesTab = ({ + nodeType, +}: { + nodeType: string; +}) => { + const [attributes, setAttributes] = useState(() => { + const record = + getDiscourseNodeSetting>(nodeType, [ + "attributes", + ]) ?? {}; + return Object.entries(record).map(([label, value]) => ({ label, value })); + }); + const [newAttribute, setNewAttribute] = useState(""); + const [overlay, setOverlay] = useState( + () => getDiscourseNodeSetting(nodeType, ["overlay"]) ?? "", + ); + + const saveAttributes = (newAttributes: AttributeEntry[]) => { + const record: Record = {}; + for (const attr of newAttributes) { + record[attr.label] = attr.value; + } + setDiscourseNodeSetting(nodeType, ["attributes"], record); + }; + + const handleChange = (label: string, newValue: string) => { + const newAttributes = attributes.map((a) => + a.label === label ? { ...a, value: newValue } : a, + ); + setAttributes(newAttributes); + saveAttributes(newAttributes); + }; + + const handleDelete = (label: string) => { + const newAttributes = attributes.filter((a) => a.label !== label); + setAttributes(newAttributes); + saveAttributes(newAttributes); + // Clear overlay if deleted attribute was selected + if (overlay === label) { + setOverlay(""); + setDiscourseNodeSetting(nodeType, ["overlay"], ""); + } + }; + + const handleAdd = () => { + if (!newAttribute.trim()) return; + const DEFAULT = "{count:Has Any Relation To:any}"; + const newAttributes = [ + ...attributes, + { label: newAttribute.trim(), value: DEFAULT }, + ]; + setAttributes(newAttributes); + saveAttributes(newAttributes); + setNewAttribute(""); + }; + + const handleOverlayChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setOverlay(newValue); + setDiscourseNodeSetting(nodeType, ["overlay"], newValue); + }; + + const attributeLabels = attributes.map((a) => a.label); + + return ( +
+
+
+ {attributes.map((a) => ( + handleChange(a.label, v)} + onDelete={() => handleDelete(a.label)} + /> + ))} +
+
+ +
+ setNewAttribute(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleAdd(); + } + }} + /> +
+
+
+ +
+ ); +}; + export default NodeAttributes; diff --git a/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx b/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx index ac26daf7f..4b80a935a 100644 --- a/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx @@ -8,10 +8,11 @@ import { ControlGroup, Checkbox, } from "@blueprintjs/core"; -import React, { useState, useMemo } from "react"; -import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; -import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromTree"; -import setInputSetting from "roamjs-components/util/setInputSetting"; +import React, { useState } from "react"; +import { + getDiscourseNodeSetting, + setDiscourseNodeSetting, +} from "./utils/accessors"; export const formatHexColor = (color: string) => { if (!color) return ""; @@ -25,24 +26,33 @@ export const formatHexColor = (color: string) => { return ""; }; -const DiscourseNodeCanvasSettings = ({ uid }: { uid: string }) => { - const tree = useMemo(() => getBasicTreeByParentUid(uid), [uid]); +const DiscourseNodeCanvasSettings = ({ nodeType }: { nodeType: string }) => { const [color, setColor] = useState(() => { - const color = getSettingValueFromTree({ tree, key: "color" }); - return formatHexColor(color); + const storedColor = getDiscourseNodeSetting(nodeType, [ + "canvasSettings", + "color", + ])!; + return formatHexColor(storedColor); }); const [alias, setAlias] = useState(() => - getSettingValueFromTree({ tree, key: "alias" }), + getDiscourseNodeSetting(nodeType, ["canvasSettings", "alias"])!, ); const [queryBuilderAlias, setQueryBuilderAlias] = useState(() => - getSettingValueFromTree({ tree, key: "query-builder-alias" }), + getDiscourseNodeSetting(nodeType, [ + "canvasSettings", + "query-builder-alias", + ])!, ); - const [isKeyImage, setIsKeyImage] = useState( - () => getSettingValueFromTree({ tree, key: "key-image" }) === "true", + const [isKeyImage, setIsKeyImage] = useState(() => + getDiscourseNodeSetting(nodeType, ["canvasSettings", "key-image"])!, ); const [keyImageOption, setKeyImageOption] = useState(() => - getSettingValueFromTree({ tree, key: "key-image-option" }), + getDiscourseNodeSetting(nodeType, [ + "canvasSettings", + "key-image-option", + ])!, ); + return (
@@ -51,14 +61,14 @@ const DiscourseNodeCanvasSettings = ({ uid }: { uid: string }) => { { setColor(e.target.value); - setInputSetting({ - blockUid: uid, - key: "color", - value: e.target.value.replace("#", ""), // remove hash to not create roam link - }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "color"], + e.target.value.replace("#", ""), // remove hash to not create roam link + ); }} /> @@ -67,11 +77,11 @@ const DiscourseNodeCanvasSettings = ({ uid }: { uid: string }) => { icon={color ? "delete" : "info-sign"} onClick={() => { setColor(""); - setInputSetting({ - blockUid: uid, - key: "color", - value: "", - }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "color"], + "", + ); }} /> @@ -83,11 +93,11 @@ const DiscourseNodeCanvasSettings = ({ uid }: { uid: string }) => { value={alias} onChange={(e) => { setAlias(e.target.value); - setInputSetting({ - blockUid: uid, - key: "alias", - value: e.target.value, - }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "alias"], + e.target.value, + ); }} /> @@ -99,17 +109,17 @@ const DiscourseNodeCanvasSettings = ({ uid }: { uid: string }) => { setIsKeyImage(target.checked); if (target.checked) { if (!keyImageOption) setKeyImageOption("first-image"); - setInputSetting({ - blockUid: uid, - key: "key-image", - value: "true", - }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "key-image"], + true, + ); } else { - setInputSetting({ - blockUid: uid, - key: "key-image", - value: "false", - }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "key-image"], + false, + ); } }} > @@ -122,19 +132,18 @@ const DiscourseNodeCanvasSettings = ({ uid }: { uid: string }) => { /> - {/* */} { const target = e.target as HTMLInputElement; setKeyImageOption(target.value); - setInputSetting({ - blockUid: uid, - key: "key-image-option", - value: target.value, - }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "key-image-option"], + target.value, + ); }} > @@ -155,11 +164,11 @@ const DiscourseNodeCanvasSettings = ({ uid }: { uid: string }) => { value={queryBuilderAlias} onChange={(e) => { setQueryBuilderAlias(e.target.value); - setInputSetting({ - blockUid: uid, - key: "query-builder-alias", - value: e.target.value, - }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "query-builder-alias"], + e.target.value, + ); }} />
diff --git a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx index 949777240..d6fa02e6b 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx @@ -4,7 +4,6 @@ import createBlock from "roamjs-components/writes/createBlock"; import { Checkbox } from "@blueprintjs/core"; import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import deleteBlock from "roamjs-components/writes/deleteBlock"; -import refreshConfigTree from "~/utils/refreshConfigTree"; import getDiscourseNodes from "~/utils/getDiscourseNodes"; import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression"; import QueryEditor from "~/components/QueryEditor"; @@ -73,9 +72,6 @@ const NodeSpecification = ({ const scratchNode = getSubTree({ tree, key: "scratch" }); Promise.all(scratchNode.children.map((c) => deleteBlock(c.uid))); } - return () => { - refreshConfigTree(); - }; }, [parentUid, setMigrated, enabled]); return (
diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx index ef3e6a752..94fed0ab2 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -1,21 +1,18 @@ -import React, { - useState, - useMemo, - useEffect, - useRef, - useCallback, -} from "react"; -import { Button, Intent } from "@blueprintjs/core"; +import React, { useState, useMemo, useEffect, useRef } from "react"; +import { Button, Intent, Label, InputGroup } from "@blueprintjs/core"; import BlocksPanel from "roamjs-components/components/ConfigPanels/BlocksPanel"; -import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; -import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; +import Description from "roamjs-components/components/Description"; import getSubTree from "roamjs-components/util/getSubTree"; import { DiscourseNode } from "~/utils/getDiscourseNodes"; import extractRef from "roamjs-components/util/extractRef"; import { getAllDiscourseNodesSince } from "~/utils/getAllDiscourseNodesSince"; import { upsertNodesToSupabaseAsContentWithEmbeddings } from "~/utils/syncDgNodesToSupabase"; -import { discourseNodeBlockToLocalConcept } from "~/utils/conceptConversion"; import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext"; +import { DiscourseNodeFlagPanel } from "./components/BlockPropSettingPanels"; +import { + getDiscourseNodeSetting, + setDiscourseNodeSetting, +} from "./utils/accessors"; const BlockRenderer = ({ uid }: { uid: string }) => { const containerRef = useRef(null); @@ -37,18 +34,26 @@ const BlockRenderer = ({ uid }: { uid: string }) => { const DiscourseNodeSuggestiveRules = ({ node, - parentUid, }: { node: DiscourseNode; - parentUid: string; }) => { - const nodeUid = node.type; + const nodeType = node.type; - const [embeddingRef, setEmbeddingRef] = useState(node.embeddingRef); + // embeddingRef needs local state for the preview to work reactively + const [embeddingRef, setEmbeddingRef] = useState( + () => getDiscourseNodeSetting(nodeType, ["embeddingRef"]) ?? "", + ); + const debounceRef = useRef(0); - useEffect(() => { - setEmbeddingRef(node.embeddingRef || ""); - }, [node.embeddingRef]); + const handleEmbeddingRefChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setEmbeddingRef(newValue); + + clearTimeout(debounceRef.current); + debounceRef.current = window.setTimeout(() => { + setDiscourseNodeSetting(nodeType, ["embeddingRef"], newValue); + }, 500); + }; const blockUidToRender = useMemo( () => extractRef(embeddingRef), @@ -58,20 +63,12 @@ const DiscourseNodeSuggestiveRules = ({ const templateUid = useMemo( () => getSubTree({ - parentUid: nodeUid, + parentUid: nodeType, key: "Template", }).uid || "", - [nodeUid], + [nodeType], ); - const handleEmbeddingRefChange = useCallback( - (e: React.ChangeEvent) => { - const newValue = e.target.value; - setEmbeddingRef(newValue); - node.embeddingRef = newValue; - }, - [node], - ); const [isUpdating, setIsUpdating] = useState(false); const handleUpdateEmbeddings = async (): Promise => { @@ -96,29 +93,27 @@ const DiscourseNodeSuggestiveRules = ({ setIsUpdating(false); } }; + return (
- + {blockUidToRender && (
@@ -127,13 +122,12 @@ const DiscourseNodeSuggestiveRules = ({
)} -
); -const useDebouncedRoamUpdater = < +const useDebouncedBlockPropUpdater = < T extends HTMLInputElement | HTMLTextAreaElement, >( - uid: string, + nodeType: string, + settingKey: string, initialValue: string, isValid: boolean, ) => { @@ -112,7 +111,7 @@ const useDebouncedRoamUpdater = < const isValidRef = useRef(isValid); isValidRef.current = isValid; - const saveToRoam = useCallback( + const saveToBlockProp = useCallback( (text: string, timeout: boolean) => { window.clearTimeout(debounceRef.current); debounceRef.current = window.setTimeout( @@ -120,33 +119,26 @@ const useDebouncedRoamUpdater = < if (!isValidRef.current) { return; } - const existingBlock = getBasicTreeByParentUid(uid)[0]; - if (existingBlock) { - if (existingBlock.text !== text) { - void updateBlock({ uid: existingBlock.uid, text }); - } - } else if (text) { - void createBlock({ parentUid: uid, node: { text } }); - } + setDiscourseNodeSetting(nodeType, [settingKey], text); }, timeout ? 500 : 0, ); }, - [uid], + [nodeType, settingKey], ); const handleChange = useCallback( (e: React.ChangeEvent) => { const newValue = e.target.value; setValue(newValue); - saveToRoam(newValue, true); + saveToBlockProp(newValue, true); }, - [saveToRoam], + [saveToBlockProp], ); const handleBlur = useCallback(() => { - saveToRoam(value, false); - }, [value, saveToRoam]); + saveToBlockProp(value, false); + }, [value, saveToBlockProp]); return { value, handleChange, handleBlur }; }; @@ -172,26 +164,15 @@ const NodeConfig = ({ onloadArgs: OnloadArgs; }) => { const suggestiveModeEnabled = useFeatureFlag("Suggestive Mode Enabled"); + // UIDs still needed for deferred complex settings (template, specification, etc.) const getUid = (key: string) => getSubTree({ parentUid: node.type, key: key, }).uid; - 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"); - const graphOverviewUid = getUid("Graph Overview"); const specificationUid = getUid("Specification"); const indexUid = getUid("Index"); - const suggestiveRulesUid = getUid("Suggestive Rules"); - const attributeNode = getSubTree({ - parentUid: node.type, - key: "Attributes", - }); const [selectedTabId, setSelectedTabId] = useState("general"); const [tagError, setTagError] = useState(""); @@ -202,8 +183,9 @@ const NodeConfig = ({ value: tagValue, handleChange: handleTagChange, handleBlur: handleTagBlurFromHook, - } = useDebouncedRoamUpdater( - tagUid, + } = useDebouncedBlockPropUpdater( + node.type, + "tag", node.tag || "", isConfigurationValid, ); @@ -211,8 +193,9 @@ const NodeConfig = ({ value: formatValue, handleChange: handleFormatChange, handleBlur: handleFormatBlurFromHook, - } = useDebouncedRoamUpdater( - formatUid, + } = useDebouncedBlockPropUpdater( + node.type, + "format", node.format, isConfigurationValid, ); @@ -220,8 +203,9 @@ const NodeConfig = ({ value: descriptionValue, handleChange: handleDescriptionChange, handleBlur: handleDescriptionBlur, - } = useDebouncedRoamUpdater( - descriptionUid, + } = useDebouncedBlockPropUpdater( + node.type, + "description", node.description || "", true, ); @@ -293,12 +277,11 @@ const NodeConfig = ({ onChange={handleDescriptionChange} onBlur={handleDescriptionBlur} /> - - - attributeNode.children.map((c) => c.text), - }} - /> +
} /> @@ -394,14 +367,13 @@ const NodeConfig = ({ title="Canvas" panel={
- - +
} @@ -412,10 +384,7 @@ const NodeConfig = ({ title="Suggestive Mode" panel={
- +
} /> diff --git a/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx index cf2bfa54c..323710df7 100644 --- a/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx +++ b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx @@ -17,6 +17,8 @@ import { setPersonalSetting, getFeatureFlag, setFeatureFlag, + getDiscourseNodeSetting, + setDiscourseNodeSetting, } from "../utils/accessors"; import type { json } from "~/utils/getBlockProps"; import type { FeatureFlags } from "../utils/zodSchema"; @@ -331,3 +333,66 @@ export const PersonalSelectPanel = ( export const PersonalMultiTextPanel = ( props: WrapperProps & { defaultValue?: string[] }, ) => ; + +const createDiscourseNodeGetter = + (nodeType: string) => + (keys: string[]): T | undefined => + getDiscourseNodeSetting(nodeType, keys); + +const createDiscourseNodeSetter = + (nodeType: string) => + (keys: string[], value: json): void => + setDiscourseNodeSetting(nodeType, keys, value); + +type DiscourseNodeWrapperProps = WrapperProps & { + nodeType: string; +}; + +export const DiscourseNodeTextPanel = ({ + nodeType, + ...props +}: DiscourseNodeWrapperProps & { defaultValue?: string; placeholder?: string }) => ( + +); + +export const DiscourseNodeFlagPanel = ({ + nodeType, + ...props +}: DiscourseNodeWrapperProps & { + defaultValue?: boolean; + disabled?: boolean; + onBeforeChange?: (checked: boolean) => Promise; + onChange?: (checked: boolean) => void; +}) => ( + +); + +export const DiscourseNodeSelectPanel = ({ + nodeType, + ...props +}: DiscourseNodeWrapperProps & { options: string[]; defaultValue?: string }) => ( + +); + +export const DiscourseNodeNumberPanel = ({ + nodeType, + ...props +}: DiscourseNodeWrapperProps & { defaultValue?: number; min?: number; max?: number }) => ( + +); diff --git a/apps/roam/src/components/settings/utils/zodSchema.example.ts b/apps/roam/src/components/settings/utils/zodSchema.example.ts index 89df7ee22..dddf65ec4 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.example.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.example.ts @@ -286,7 +286,7 @@ const personalSettings: PersonalSettings = { }, }, }, - "Personal Node Menu Trigger": ";;", + "Personal Node Menu Trigger": { key: ";", modifiers: 0 }, "Node Search Menu Trigger": "//", "Discourse Tool Shortcut": "d", "Discourse Context Overlay": true, @@ -314,7 +314,7 @@ const personalSettings: PersonalSettings = { const defaultPersonalSettings: PersonalSettings = { "Left Sidebar": {}, - "Personal Node Menu Trigger": "", + "Personal Node Menu Trigger": { key: "", modifiers: 0 }, "Node Search Menu Trigger": "", "Discourse Tool Shortcut": "", "Discourse Context Overlay": false, diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts index 8c4bb1705..06f5f5a86 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -220,7 +220,9 @@ export const QuerySettingsSchema = z.object({ export const PersonalSettingsSchema = z.object({ "Left Sidebar": LeftSidebarPersonalSettingsSchema, - "Personal Node Menu Trigger": z.string().default(""), + "Personal Node Menu Trigger": z + .object({ key: z.string(), modifiers: z.number() }) + .default({ key: "", modifiers: 0 }), "Node Search Menu Trigger": z.string().default(""), "Discourse Tool Shortcut": z.string().default(""), "Discourse Context Overlay": z.boolean().default(false), diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index 5b71e2785..3bc256ab9 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -38,7 +38,10 @@ import { DISALLOW_DIAGNOSTICS, } from "./data/userSettings"; import { initSchema } from "./components/settings/utils/init"; -import { setupPullWatchSettings } from "./components/settings/utils/pullWatchers"; +import { + setupPullWatchSettings, + setupPullWatchDiscourseNodes, +} from "./components/settings/utils/pullWatchers"; import { getFeatureFlag } from "./components/settings/utils/accessors"; export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*"; @@ -74,8 +77,9 @@ export default runExtension(async (onloadArgs) => { initPluginTimer(); - const { blockUids } = await initSchema(); - const cleanupPullWatch = setupPullWatchSettings(blockUids); + const { blockUids, nodePageUids } = await initSchema(); + const cleanupPullWatchSettings = setupPullWatchSettings(blockUids); + const cleanupPullWatchNodes = setupPullWatchDiscourseNodes(nodePageUids); addGraphViewNodeStyling(); registerCommandPaletteCommands(onloadArgs); createSettingsPanel(onloadArgs); @@ -157,7 +161,8 @@ export default runExtension(async (onloadArgs) => { ], observers: observers, unload: () => { - cleanupPullWatch(); + cleanupPullWatchSettings(); + cleanupPullWatchNodes(); setSyncActivity(false); window.roamjs.extension?.smartblocks?.unregisterCommand("QUERYBUILDER"); // @ts-expect-error - tldraw throws a warning on multiple loads diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 08d859c5d..b91894455 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -35,7 +35,6 @@ import { getModifiersFromCombo, render as renderDiscourseNodeMenu, } from "~/components/DiscourseNodeMenu"; -import { IKeyCombo } from "@blueprintjs/core"; import { configPageTabs } from "~/utils/configPageTabs"; import { renderDiscourseNodeSearchMenu } from "~/components/DiscourseNodeSearchMenu"; import { @@ -244,10 +243,10 @@ export const initObservers = async ({ const globalTrigger = ( getGlobalSetting(["Trigger"]) || "\\" ).trim(); - const personalTriggerCombo = - (onloadArgs.extensionAPI.settings.get( - "personal-node-menu-trigger", - ) as IKeyCombo) || undefined; + const personalTriggerCombo = getPersonalSetting<{ + key: string; + modifiers: number; + }>(["Personal Node Menu Trigger"]); const personalTrigger = personalTriggerCombo?.key; const personalModifiers = getModifiersFromCombo(personalTriggerCombo); @@ -310,7 +309,7 @@ export const initObservers = async ({ } }; - const customTrigger = getSetting("node-search-trigger", "@"); + const customTrigger = getPersonalSetting(["Node Search Menu Trigger"])!; const discourseNodeSearchTriggerListener = (e: Event) => { const evt = e as KeyboardEvent; diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 63d208230..dd6b3ba84 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -144,8 +144,9 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { const renderSettingsPopup = () => renderSettings({ onloadArgs }); const toggleDiscourseContextOverlay = () => { - const currentValue = - getPersonalSetting(["Discourse Context Overlay"]) ?? false; + const currentValue = getPersonalSetting([ + "Discourse Context Overlay", + ])!; const newValue = !currentValue; setPersonalSetting(["Discourse Context Overlay"], newValue); const overlayHandler = getOverlayHandler(onloadArgs); diff --git a/apps/roam/src/utils/renderNodeConfigPage.ts b/apps/roam/src/utils/renderNodeConfigPage.ts index 0e5d223dc..7542397ec 100644 --- a/apps/roam/src/utils/renderNodeConfigPage.ts +++ b/apps/roam/src/utils/renderNodeConfigPage.ts @@ -1,7 +1,6 @@ import React from "react"; import CustomPanel from "roamjs-components/components/ConfigPanels/CustomPanel"; import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; -import SelectPanel from "roamjs-components/components/ConfigPanels/SelectPanel"; import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; import BlocksPanel from "roamjs-components/components/ConfigPanels/BlocksPanel"; @@ -9,17 +8,14 @@ import { Field, CustomField, TextField, - SelectField, FieldPanel, FlagField, } from "roamjs-components/components/ConfigPanels/types"; -import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import { OnloadArgs } from "roamjs-components/types"; -import { getSubTree } from "roamjs-components/util"; import { DiscourseNodeIndex, DiscourseNodeSpecification, - DiscourseNodeAttributes, + DiscourseNodeAttributesTab, DiscourseNodeCanvasSettings, } from "~/components"; import getDiscourseNodes from "~/utils/getDiscourseNodes"; @@ -116,29 +112,22 @@ export const renderNodeConfigPage = ({ description: `A set of derived properties about the node based on queryable data.`, Panel: CustomPanel, options: { - component: DiscourseNodeAttributes, + component: () => + React.createElement(DiscourseNodeAttributesTab, { + nodeType: node.type, + }), }, } as Field, // @ts-ignore - { - title: "Overlay", - description: `Select which attribute is used for the Discourse Overlay`, - Panel: SelectPanel, - options: { - items: () => - getSubTree({ - parentUid: getPageUidByPageTitle(title), - key: "Attributes", - }).children.map((c) => c.text), - }, - } as Field, - // @ts-ignore { title: "Canvas", description: `Various options for this node in the Discourse Canvas`, Panel: CustomPanel, options: { - component: DiscourseNodeCanvasSettings, + component: () => + React.createElement(DiscourseNodeCanvasSettings, { + nodeType: node.type, + }), }, } as Field, // @ts-ignore @@ -153,10 +142,9 @@ export const renderNodeConfigPage = ({ title: "Suggestive Rules", Panel: CustomPanel, options: { - component: ({ uid }) => + component: () => React.createElement(DiscourseNodeSuggestiveRules, { node, - parentUid: uid, }), }, } as Field,