From 6e0ba3144eb9951cc3daa74955072ce0c8851d90 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 24 Feb 2026 19:13:19 +0530 Subject: [PATCH 1/5] Land orphaned Graphite stack PRs (#764, #765, #769, #793, #794, #805) These PRs were squash-merged into parent branches instead of main due to a Graphite stack merge race condition. Their changes never landed on main despite showing as merged in Graphite. Includes: - #764 ENG-1280: Port query builder/editor for block props - #765 ENG-1291: Port discourse node specification (dual-write) - #769 ENG-1290: Port discourse node config panel - #793 ENG-1440: Port page groups settings in suggestive mode - #794 ENG-1438: Port keyboard shortcut keys/triggers - #805 ENG-1328: Port small blueprint misc settings --- .../roam/src/components/DiscourseNodeMenu.tsx | 25 +-- .../components/DiscourseNodeSearchMenu.tsx | 4 +- .../src/components/settings/AdminPanel.tsx | 4 + .../components/settings/DefaultFilters.tsx | 42 ++--- .../settings/DiscourseNodeAttributes.tsx | 52 ++++++- .../settings/DiscourseNodeCanvasSettings.tsx | 66 +++++--- .../settings/DiscourseNodeConfigPanel.tsx | 36 +++-- .../settings/HomePersonalSettings.tsx | 1 + .../settings/KeyboardShortcutInput.tsx | 73 ++------- .../src/components/settings/NodeConfig.tsx | 143 +++--------------- .../components/settings/PageGroupPanel.tsx | 43 ++++-- .../src/components/settings/utils/init.ts | 1 + .../settings/utils/zodSchema.example.ts | 8 +- .../components/settings/utils/zodSchema.ts | 11 +- apps/roam/src/index.ts | 2 +- 15 files changed, 243 insertions(+), 268 deletions(-) diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index d73975c2f..f458e80a7 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -27,6 +27,7 @@ import { getNewDiscourseNodeText } from "~/utils/formatUtils"; import { OnloadArgs } from "roamjs-components/types"; import { formatHexColor } from "./settings/DiscourseNodeCanvasSettings"; import posthog from "posthog-js"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; type Props = { textarea?: HTMLTextAreaElement; @@ -406,6 +407,13 @@ export const getModifiersFromCombo = (comboKey: IKeyCombo) => { ].filter(Boolean); }; +export const comboToString = (combo: IKeyCombo): string => { + if (!combo.key) return ""; + const modifiers = getModifiersFromCombo(combo); + const comboString = [...modifiers, combo.key].join("+"); + return normalizeKeyCombo(comboString).join("+"); +}; + export const NodeMenuTriggerComponent = ({ extensionAPI, }: { @@ -427,19 +435,15 @@ export const NodeMenuTriggerComponent = ({ const comboObj = getKeyCombo(e.nativeEvent); if (!comboObj.key) return; - setComboKey({ key: comboObj.key, modifiers: comboObj.modifiers }); - extensionAPI.settings.set("personal-node-menu-trigger", comboObj); + const combo = { key: comboObj.key, modifiers: comboObj.modifiers }; + setComboKey(combo); + void extensionAPI.settings.set("personal-node-menu-trigger", combo); + setPersonalSetting(["Personal node menu trigger"], combo); }, [extensionAPI], ); - const shortcut = useMemo(() => { - if (!comboKey.key) return ""; - - const modifiers = getModifiersFromCombo(comboKey); - const comboString = [...modifiers, comboKey.key].join("+"); - return normalizeKeyCombo(comboString).join("+"); - }, [comboKey]); + const shortcut = useMemo(() => comboToString(comboKey), [comboKey]); return ( { setComboKey({ modifiers: 0, key: "" }); - extensionAPI.settings.set("personal-node-menu-trigger", ""); + void extensionAPI.settings.set("personal-node-menu-trigger", ""); + setPersonalSetting(["Personal node menu trigger"], ""); }} minimal /> diff --git a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx index 021fc005c..abef73705 100644 --- a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx @@ -26,6 +26,7 @@ import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpr import { Result } from "~/utils/types"; import { getSetting } from "~/utils/extensionSettings"; import MiniSearch from "minisearch"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; type Props = { textarea: HTMLTextAreaElement; @@ -724,7 +725,8 @@ export const NodeSearchMenuTriggerSetting = ({ .trim(); setNodeSearchTrigger(trigger); - extensionAPI.settings.set("node-search-trigger", trigger); + void extensionAPI.settings.set("node-search-trigger", trigger); + setPersonalSetting(["Node search menu trigger"], trigger); }; return ( { return ( @@ -377,6 +378,7 @@ const FeatureFlagsTab = (): React.ReactElement => { setSuggestiveModeUid(undefined); } setSuggestiveModeEnabled(false); + setFeatureFlag("Suggestive mode enabled", false); } }} labelElement={ @@ -399,6 +401,7 @@ const FeatureFlagsTab = (): React.ReactElement => { }).then((uid) => { setSuggestiveModeUid(uid); setSuggestiveModeEnabled(true); + setFeatureFlag("Suggestive mode enabled", true); setIsAlertOpen(false); setIsInstructionOpen(true); }); @@ -447,6 +450,7 @@ const FeatureFlagsTab = (): React.ReactElement => { void setSetting(USE_REIFIED_RELATIONS, target.checked).catch( () => undefined, ); + setFeatureFlag("Reified relation triples", target.checked); posthog.capture("Reified Relations: Toggled", { enabled: target.checked, }); diff --git a/apps/roam/src/components/settings/DefaultFilters.tsx b/apps/roam/src/components/settings/DefaultFilters.tsx index f9daeb85f..693e98f74 100644 --- a/apps/roam/src/components/settings/DefaultFilters.tsx +++ b/apps/roam/src/components/settings/DefaultFilters.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react"; import type { OnloadArgs } from "roamjs-components/types"; import type { Filters } from "roamjs-components/components/Filter"; import posthog from "posthog-js"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; // // TODO - REWORK THIS COMPONENT @@ -124,28 +125,27 @@ const DefaultFilters = ({ ); useEffect(() => { - extensionAPI.settings.set( - "default-filters", - Object.fromEntries( - Object.entries(filters).map(([k, v]) => [ - k, - { - includes: Object.fromEntries( - Object.entries(v.includes || {}).map(([k, v]) => [ - k, - Array.from(v), - ]), - ), - excludes: Object.fromEntries( - Object.entries(v.excludes || {}).map(([k, v]) => [ - k, - Array.from(v), - ]), - ), - }, - ]), - ), + const serialized = Object.fromEntries( + Object.entries(filters).map(([k, v]) => [ + k, + { + includes: Object.fromEntries( + Object.entries(v.includes || {}).map(([k, v]) => [ + k, + Array.from(v), + ]), + ), + excludes: Object.fromEntries( + Object.entries(v.excludes || {}).map(([k, v]) => [ + k, + Array.from(v), + ]), + ), + }, + ]), ); + void extensionAPI.settings.set("default-filters", serialized); + setPersonalSetting(["Query", "Default filters"], serialized); }, [filters]); return (
void; onDelete: () => void }) => { + onSync, +}: Attribute & { + onChange: (v: string) => void; + onDelete: () => void; + onSync?: () => void; +}) => { const timeoutRef = useRef(0); return (
@@ -53,7 +60,16 @@ const NodeAttribute = ({ ); }; -const NodeAttributes = ({ uid }: { uid: string }) => { +const toRecord = (attrs: Attribute[]): Record => + Object.fromEntries(attrs.map((a) => [a.label, a.value])); + +const NodeAttributes = ({ + uid, + nodeType, +}: { + uid: string; + nodeType: string; +}) => { const [attributes, setAttributes] = useState(() => getBasicTreeByParentUid(uid).map((t) => ({ uid: t.uid, @@ -61,6 +77,15 @@ const NodeAttributes = ({ uid }: { uid: string }) => { value: t.children[0]?.text, })), ); + const attributesRef = useRef(attributes); + attributesRef.current = attributes; + const syncToBlockProps = () => { + setDiscourseNodeSetting( + nodeType, + ["attributes"], + toRecord(attributesRef.current), + ); + }; const [newAttribute, setNewAttribute] = useState(""); return (
@@ -77,10 +102,17 @@ const NodeAttributes = ({ uid }: { uid: string }) => { ) } onDelete={() => - deleteBlock(a.uid).then(() => - setAttributes(attributes.filter((aa) => a.uid !== aa.uid)), - ) + deleteBlock(a.uid).then(() => { + const updated = attributes.filter((aa) => a.uid !== aa.uid); + setAttributes(updated); + setDiscourseNodeSetting( + nodeType, + ["attributes"], + toRecord(updated), + ); + }) } + onSync={syncToBlockProps} /> ))}
@@ -105,11 +137,17 @@ const NodeAttributes = ({ uid }: { uid: string }) => { parentUid: uid, order: attributes.length, }).then((uid) => { - setAttributes([ + const updated = [ ...attributes, { uid, label: newAttribute, value: DEFAULT }, - ]); + ]; + setAttributes(updated); setNewAttribute(""); + setDiscourseNodeSetting( + nodeType, + ["attributes"], + toRecord(updated), + ); }); }} /> diff --git a/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx b/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx index ebc47d1ad..287f4194b 100644 --- a/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx @@ -11,7 +11,11 @@ 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 { DiscourseNodeFlagPanel } from "./components/BlockPropSettingPanels"; +import { + DiscourseNodeFlagPanel, + DiscourseNodeTextPanel, +} from "./components/BlockPropSettingPanels"; +import { setDiscourseNodeSetting } from "~/components/settings/utils/accessors"; export const formatHexColor = (color: string) => { if (!color) return ""; @@ -37,9 +41,7 @@ const DiscourseNodeCanvasSettings = ({ const color = getSettingValueFromTree({ tree, key: "color" }); return formatHexColor(color); }); - const [alias, setAlias] = useState(() => - getSettingValueFromTree({ tree, key: "alias" }), - ); + const alias = getSettingValueFromTree({ tree, key: "alias" }); const [queryBuilderAlias, setQueryBuilderAlias] = useState(() => getSettingValueFromTree({ tree, key: "query-builder-alias" }), ); @@ -60,12 +62,18 @@ const DiscourseNodeCanvasSettings = ({ type={"color"} value={color} onChange={(e) => { + const colorValue = e.target.value.replace("#", ""); // remove hash to not create roam link setColor(e.target.value); void setInputSetting({ blockUid: uid, key: "color", - value: e.target.value.replace("#", ""), // remove hash to not create roam link + value: colorValue, }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "color"], + colorValue, + ); }} /> @@ -79,25 +87,30 @@ const DiscourseNodeCanvasSettings = ({ key: "color", value: "", }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "color"], + "", + ); }} />
- + { + void setInputSetting({ + blockUid: uid, + key: "alias", + value: val, + }); + }} + /> @@ -145,12 +163,18 @@ const DiscourseNodeCanvasSettings = ({ disabled={keyImageOption !== "query-builder" || !isKeyImage} value={queryBuilderAlias} onChange={(e) => { - setQueryBuilderAlias(e.target.value); + const val = e.target.value; + setQueryBuilderAlias(val); void setInputSetting({ blockUid: uid, key: "query-builder-alias", - value: e.target.value, + value: val, }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "query-builder-alias"], + val, + ); }} />
diff --git a/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx b/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx index ea5db06cc..8d388189e 100644 --- a/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx @@ -13,9 +13,14 @@ import refreshConfigTree from "~/utils/refreshConfigTree"; import createPage from "roamjs-components/writes/createPage"; import type { CustomField } from "roamjs-components/components/ConfigPanels/types"; import posthog from "posthog-js"; -import getDiscourseRelations from "~/utils/getDiscourseRelations"; +import getDiscourseRelations, { + type DiscourseRelation, +} from "~/utils/getDiscourseRelations"; import { deleteBlock } from "roamjs-components/writes"; import { formatHexColor } from "./DiscourseNodeCanvasSettings"; +import setBlockProps from "~/utils/setBlockProps"; +import { DiscourseNodeSchema } from "./utils/zodSchema"; +import { getGlobalSettings, setGlobalSetting } from "./utils/accessors"; type DiscourseNodeConfigPanelProps = React.ComponentProps< CustomField["options"]["component"] @@ -38,7 +43,9 @@ const DiscourseNodeConfigPanel: React.FC = ({ const [isAlertOpen, setIsAlertOpen] = useState(false); const [alertMessage, setAlertMessage] = useState(""); - const [affectedRelations, setAffectedRelations] = useState([]); + const [affectedRelations, setAffectedRelations] = useState< + DiscourseRelation[] + >([]); const [nodeTypeIdToDelete, setNodeTypeIdToDelete] = useState(""); const navigateToNode = (uid: string) => { if (isPopup) { @@ -72,13 +79,15 @@ const DiscourseNodeConfigPanel: React.FC = ({ className="select-none" disabled={!label} onClick={() => { + const shortcut = label.slice(0, 1).toUpperCase(); + const format = `[[${label.slice(0, 3).toUpperCase()}]] - {content}`; posthog.capture("Discourse Node: Type Created", { label: label }); - createPage({ + void createPage({ title: `discourse-graph/nodes/${label}`, tree: [ { text: "Shortcut", - children: [{ text: label.slice(0, 1).toUpperCase() }], + children: [{ text: shortcut }], }, { text: "Tag", @@ -86,14 +95,20 @@ const DiscourseNodeConfigPanel: React.FC = ({ }, { text: "Format", - children: [ - { - text: `[[${label.slice(0, 3).toUpperCase()}]] - {content}`, - }, - ], + children: [{ text: format }], }, ], }).then((valueUid) => { + setBlockProps( + valueUid, + DiscourseNodeSchema.parse({ + text: label, + type: valueUid, + shortcut, + format, + backedBy: "user", + }), + ); setNodes([ ...nodes, { @@ -222,6 +237,9 @@ const DiscourseNodeConfigPanel: React.FC = ({ throw error; }); } + const relations = { ...getGlobalSettings().Relations }; + for (const rel of affectedRelations) delete relations[rel.id]; + setGlobalSetting(["Relations"], relations); deleteNodeType(nodeTypeIdToDelete); } catch (error) { console.error( diff --git a/apps/roam/src/components/settings/HomePersonalSettings.tsx b/apps/roam/src/components/settings/HomePersonalSettings.tsx index 01083caae..f4d2c74a3 100644 --- a/apps/roam/src/components/settings/HomePersonalSettings.tsx +++ b/apps/roam/src/components/settings/HomePersonalSettings.tsx @@ -58,6 +58,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { { - const platform = - typeof navigator !== "undefined" ? navigator.platform : undefined; - return platform == null ? false : /Mac|iPod|iPhone|iPad/.test(platform); -}; - -const MODIFIER_BIT_MASKS = { - alt: 1, - ctrl: 2, - meta: 4, - shift: 8, -}; - -const ALIASES: { [key: string]: string } = { - cmd: "meta", - command: "meta", - escape: "esc", - minus: "-", - mod: isMac() ? "meta" : "ctrl", - option: "alt", - plus: "+", - return: "enter", - win: "meta", -}; - -const normalizeKeyCombo = (combo: string) => { - const keys = combo.replace(/\s/g, "").split("+"); - return keys.map((key) => { - const keyName = ALIASES[key] != null ? ALIASES[key] : key; - return keyName === "meta" ? (isMac() ? "cmd" : "win") : keyName; - }); -}; - -const getModifiersFromCombo = (comboKey: IKeyCombo) => { - if (!comboKey) return []; - return [ - comboKey.modifiers & MODIFIER_BIT_MASKS.alt && "alt", - comboKey.modifiers & MODIFIER_BIT_MASKS.ctrl && "ctrl", - comboKey.modifiers & MODIFIER_BIT_MASKS.shift && "shift", - comboKey.modifiers & MODIFIER_BIT_MASKS.meta && "meta", - ].filter(Boolean); -}; - const KeyboardShortcutInput = ({ onloadArgs, settingKey, + blockPropKey, label, description, placeholder = "Click to set shortcut", @@ -104,6 +64,7 @@ const KeyboardShortcutInput = ({ extensionAPI.settings .set(settingKey, comboObj) .catch(() => console.error("Failed to set setting")); + setPersonalSetting([blockPropKey], comboObj); } return; } @@ -112,28 +73,26 @@ const KeyboardShortcutInput = ({ const comboObj = getKeyCombo(e.nativeEvent); if (!comboObj.key) return; - setComboKey({ key: comboObj.key, modifiers: comboObj.modifiers }); + const combo = { key: comboObj.key, modifiers: comboObj.modifiers }; + setComboKey(combo); extensionAPI.settings - .set(settingKey, comboObj) + .set(settingKey, combo) .catch(() => console.error("Failed to set setting")); + setPersonalSetting([blockPropKey], combo); }, - [extensionAPI, settingKey], + [extensionAPI, settingKey, blockPropKey], ); - const shortcut = useMemo(() => { - if (!comboKey.key) return ""; - - const modifiers = getModifiersFromCombo(comboKey); - const comboString = [...modifiers, comboKey.key].join("+"); - return normalizeKeyCombo(comboString).join("+"); - }, [comboKey]); + const shortcut = useMemo(() => comboToString(comboKey), [comboKey]); const handleClear = useCallback(() => { - setComboKey({ modifiers: 0, key: "" }); + const clearedCombo = { modifiers: 0, key: "" }; + setComboKey(clearedCombo); extensionAPI.settings - .set(settingKey, { modifiers: 0, key: "" }) + .set(settingKey, clearedCombo) .catch(() => console.error("Failed to set setting")); - }, [extensionAPI, settingKey]); + setPersonalSetting([blockPropKey], clearedCombo); + }, [extensionAPI, settingKey, blockPropKey]); return (