From 90d5792843fb2cff8830de2ea41b93b6e7993f62 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 22 Jun 2026 01:11:27 -0600 Subject: [PATCH 1/4] Refine color debounce cleanup --- .../src/components/settings/NodeConfig.tsx | 116 +++++++++++++----- 1 file changed, 87 insertions(+), 29 deletions(-) diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index 3078f4c72..7ec9c0c8f 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useEffect } from "react"; +import React, { useState, useCallback, useEffect, useRef } from "react"; import getDiscourseNodes, { DiscourseNode } from "~/utils/getDiscourseNodes"; import DualWriteBlocksPanel from "./components/EphemeralBlocksPanel"; import { getSubTree } from "roamjs-components/util"; @@ -45,6 +45,31 @@ export const getCleanTagText = (tag: string): string => { return tag.replace(/^#+/, "").trim().toUpperCase(); }; +const COLOR_WRITE_DEBOUNCE_MS = 300; + +type PendingColorWrite = { + blockUid: string; + nodeType: string; + value: string; +}; + +const writeColorSetting = ({ + blockUid, + nodeType, + value, +}: PendingColorWrite): void => { + void setInputSetting({ + blockUid, + key: "color", + value, + }); + setDiscourseNodeSetting( + nodeType, + [DISCOURSE_NODE_KEYS.canvasSettings, CANVAS_KEYS.color], + value, + ); +}; + const generateTagPlaceholder = (node: DiscourseNode): string => { // Extract first reference from format like [[CLM]], [[QUE]], [[EVD]] const referenceMatch = node.format.match(/\[\[([A-Z]+)\]\]/); @@ -89,6 +114,10 @@ const NodeConfig = ({ const [color, setColor] = useState(() => formatHexColor(node.canvasSettings?.color ?? ""), ); + const colorWriteTimeoutRef = useRef | null>( + null, + ); + const pendingColorWriteRef = useRef(null); const [selectedTabId, setSelectedTabId] = useState("general"); const [tagError, setTagError] = useState(""); @@ -98,6 +127,57 @@ const NodeConfig = ({ const [tagValue, setTagValue] = useState(node.tag || ""); const [formatValue, setFormatValue] = useState(node.format || ""); const [shortcutValue, setShortcutValue] = useState(node.shortcut || ""); + + const clearColorWriteTimeout = useCallback((): void => { + if (!colorWriteTimeoutRef.current) return; + + clearTimeout(colorWriteTimeoutRef.current); + colorWriteTimeoutRef.current = null; + }, []); + + const persistColor = useCallback( + (colorValue: string): void => { + const colorWrite = { + blockUid: canvasUid, + nodeType: node.type, + value: colorValue, + }; + + pendingColorWriteRef.current = null; + writeColorSetting(colorWrite); + }, + [canvasUid, node.type], + ); + + const persistColorAfterPause = useCallback( + (colorValue: string): void => { + const colorWrite = { + blockUid: canvasUid, + nodeType: node.type, + value: colorValue, + }; + + clearColorWriteTimeout(); + pendingColorWriteRef.current = colorWrite; + colorWriteTimeoutRef.current = setTimeout(() => { + writeColorSetting(colorWrite); + pendingColorWriteRef.current = null; + colorWriteTimeoutRef.current = null; + }, COLOR_WRITE_DEBOUNCE_MS); + }, + [canvasUid, clearColorWriteTimeout, node.type], + ); + + useEffect( + () => () => { + clearColorWriteTimeout(); + if (!pendingColorWriteRef.current) return; + + writeColorSetting(pendingColorWriteRef.current); + pendingColorWriteRef.current = null; + }, + [clearColorWriteTimeout], + ); const validate = useCallback( ({ tag, @@ -274,21 +354,10 @@ const NodeConfig = ({ 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: canvasUid, - key: "color", - value: colorValue, - }); - setDiscourseNodeSetting( - node.type, - [ - DISCOURSE_NODE_KEYS.canvasSettings, - CANVAS_KEYS.color, - ], - colorValue, - ); + const nextColor = e.target.value; + const colorValue = nextColor.replace("#", ""); // remove hash to not create roam link + setColor(nextColor); + persistColorAfterPause(colorValue); }} /> @@ -296,20 +365,9 @@ const NodeConfig = ({ className={"ml-2 align-middle opacity-80"} icon={color ? "delete" : "info-sign"} onClick={() => { + clearColorWriteTimeout(); setColor(""); - void setInputSetting({ - blockUid: canvasUid, - key: "color", - value: "", - }); - setDiscourseNodeSetting( - node.type, - [ - DISCOURSE_NODE_KEYS.canvasSettings, - CANVAS_KEYS.color, - ], - "", - ); + persistColor(""); }} /> From 598c04e02193c1fff9f462b086e17fb77fbb7c22 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 22 Jun 2026 01:28:23 -0600 Subject: [PATCH 2/4] Flush node color writes before settings refresh --- .../src/components/settings/NodeConfig.tsx | 31 +++++++++++++------ .../roam/src/components/settings/Settings.tsx | 3 +- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index 7ec9c0c8f..a5f14daa7 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -70,6 +70,14 @@ const writeColorSetting = ({ ); }; +const pendingColorWriteFlushers = new Set<() => void>(); + +export const flushPendingNodeColorWrites = (): void => { + pendingColorWriteFlushers.forEach((flushPendingColorWrite) => + flushPendingColorWrite(), + ); +}; + const generateTagPlaceholder = (node: DiscourseNode): string => { // Extract first reference from format like [[CLM]], [[QUE]], [[EVD]] const referenceMatch = node.format.match(/\[\[([A-Z]+)\]\]/); @@ -168,16 +176,21 @@ const NodeConfig = ({ [canvasUid, clearColorWriteTimeout, node.type], ); - useEffect( - () => () => { - clearColorWriteTimeout(); - if (!pendingColorWriteRef.current) return; + const flushPendingColorWrite = useCallback((): void => { + clearColorWriteTimeout(); + if (!pendingColorWriteRef.current) return; - writeColorSetting(pendingColorWriteRef.current); - pendingColorWriteRef.current = null; - }, - [clearColorWriteTimeout], - ); + writeColorSetting(pendingColorWriteRef.current); + pendingColorWriteRef.current = null; + }, [clearColorWriteTimeout]); + + useEffect(() => { + pendingColorWriteFlushers.add(flushPendingColorWrite); + return () => { + pendingColorWriteFlushers.delete(flushPendingColorWrite); + flushPendingColorWrite(); + }; + }, [flushPendingColorWrite]); const validate = useCallback( ({ tag, diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 92cc46fcf..2031d30d6 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -21,7 +21,7 @@ import DiscourseNodeConfigPanel from "./DiscourseNodeConfigPanel"; import getDiscourseNodes, { excludeDefaultNodes, } from "~/utils/getDiscourseNodes"; -import NodeConfig from "./NodeConfig"; +import NodeConfig, { flushPendingNodeColorWrites } from "./NodeConfig"; import HomePersonalSettings from "./HomePersonalSettings"; import CanvasShortcutSettings from "./CanvasShortcutSettings"; import refreshConfigTree from "~/utils/refreshConfigTree"; @@ -148,6 +148,7 @@ export const SettingsDialog = ({ { + flushPendingNodeColorWrites(); refreshConfigTree(); onClose?.(); }} From 7c0e378e917e4962a4a8d51f0471906ba419dce2 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 22 Jun 2026 01:45:44 -0600 Subject: [PATCH 3/4] Refactor color setting logic in NodeConfig component; update debounce timing and remove unused flush function --- .../src/components/settings/NodeConfig.tsx | 221 ++++++++---------- .../roam/src/components/settings/Settings.tsx | 3 +- 2 files changed, 96 insertions(+), 128 deletions(-) diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index a5f14daa7..8357af740 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -45,36 +45,103 @@ export const getCleanTagText = (tag: string): string => { return tag.replace(/^#+/, "").trim().toUpperCase(); }; -const COLOR_WRITE_DEBOUNCE_MS = 300; +const COLOR_WRITE_DEBOUNCE_MS = 250; -type PendingColorWrite = { - blockUid: string; +type DiscourseNodeColorSettingProps = { + canvasUid: string; nodeType: string; - value: string; + initialColor?: string; + tagValue: string; }; -const writeColorSetting = ({ - blockUid, +const DiscourseNodeColorSetting = ({ + canvasUid, nodeType, - value, -}: PendingColorWrite): void => { - void setInputSetting({ - blockUid, - key: "color", - value, - }); - setDiscourseNodeSetting( - nodeType, - [DISCOURSE_NODE_KEYS.canvasSettings, CANVAS_KEYS.color], - value, + initialColor, + tagValue, +}: DiscourseNodeColorSettingProps): React.ReactElement => { + const [color, setColor] = useState(() => + formatHexColor(initialColor ?? ""), ); -}; + const colorWriteTimeoutRef = useRef(null); -const pendingColorWriteFlushers = new Set<() => void>(); + const persistColorValue = useCallback( + (colorValue: string): void => { + void setInputSetting({ + blockUid: canvasUid, + key: "color", + value: colorValue, + }); + setDiscourseNodeSetting( + nodeType, + [DISCOURSE_NODE_KEYS.canvasSettings, CANVAS_KEYS.color], + colorValue, + ); + }, + [canvasUid, nodeType], + ); + + useEffect(() => { + return () => { + if (!colorWriteTimeoutRef.current) return; -export const flushPendingNodeColorWrites = (): void => { - pendingColorWriteFlushers.forEach((flushPendingColorWrite) => - flushPendingColorWrite(), + window.clearTimeout(colorWriteTimeoutRef.current); + }; + }, []); + + const persistColorAfterPause = (colorValue: string): void => { + if (colorWriteTimeoutRef.current) { + window.clearTimeout(colorWriteTimeoutRef.current); + colorWriteTimeoutRef.current = null; + } + colorWriteTimeoutRef.current = window.setTimeout(() => { + persistColorValue(colorValue); + colorWriteTimeoutRef.current = null; + }, COLOR_WRITE_DEBOUNCE_MS); + }; + + return ( + <> + {tagValue && ( +
+ Preview: + + #{tagValue.replace(/^#/, "")} + +
+ )} + + ); }; @@ -119,14 +186,6 @@ const NodeConfig = ({ key: "Attributes", }); - const [color, setColor] = useState(() => - formatHexColor(node.canvasSettings?.color ?? ""), - ); - const colorWriteTimeoutRef = useRef | null>( - null, - ); - const pendingColorWriteRef = useRef(null); - const [selectedTabId, setSelectedTabId] = useState("general"); const [tagError, setTagError] = useState(""); const [formatError, setFormatError] = useState(""); @@ -135,62 +194,6 @@ const NodeConfig = ({ const [tagValue, setTagValue] = useState(node.tag || ""); const [formatValue, setFormatValue] = useState(node.format || ""); const [shortcutValue, setShortcutValue] = useState(node.shortcut || ""); - - const clearColorWriteTimeout = useCallback((): void => { - if (!colorWriteTimeoutRef.current) return; - - clearTimeout(colorWriteTimeoutRef.current); - colorWriteTimeoutRef.current = null; - }, []); - - const persistColor = useCallback( - (colorValue: string): void => { - const colorWrite = { - blockUid: canvasUid, - nodeType: node.type, - value: colorValue, - }; - - pendingColorWriteRef.current = null; - writeColorSetting(colorWrite); - }, - [canvasUid, node.type], - ); - - const persistColorAfterPause = useCallback( - (colorValue: string): void => { - const colorWrite = { - blockUid: canvasUid, - nodeType: node.type, - value: colorValue, - }; - - clearColorWriteTimeout(); - pendingColorWriteRef.current = colorWrite; - colorWriteTimeoutRef.current = setTimeout(() => { - writeColorSetting(colorWrite); - pendingColorWriteRef.current = null; - colorWriteTimeoutRef.current = null; - }, COLOR_WRITE_DEBOUNCE_MS); - }, - [canvasUid, clearColorWriteTimeout, node.type], - ); - - const flushPendingColorWrite = useCallback((): void => { - clearColorWriteTimeout(); - if (!pendingColorWriteRef.current) return; - - writeColorSetting(pendingColorWriteRef.current); - pendingColorWriteRef.current = null; - }, [clearColorWriteTimeout]); - - useEffect(() => { - pendingColorWriteFlushers.add(flushPendingColorWrite); - return () => { - pendingColorWriteFlushers.delete(flushPendingColorWrite); - flushPendingColorWrite(); - }; - }, [flushPendingColorWrite]); const validate = useCallback( ({ tag, @@ -346,47 +349,13 @@ const NodeConfig = ({ parentUid={node.type} uid={tagUid} /> - {tagValue && ( -
- - Preview: - - - #{tagValue.replace(/^#/, "")} - -
- )} - <> - - + } /> diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 2031d30d6..92cc46fcf 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -21,7 +21,7 @@ import DiscourseNodeConfigPanel from "./DiscourseNodeConfigPanel"; import getDiscourseNodes, { excludeDefaultNodes, } from "~/utils/getDiscourseNodes"; -import NodeConfig, { flushPendingNodeColorWrites } from "./NodeConfig"; +import NodeConfig from "./NodeConfig"; import HomePersonalSettings from "./HomePersonalSettings"; import CanvasShortcutSettings from "./CanvasShortcutSettings"; import refreshConfigTree from "~/utils/refreshConfigTree"; @@ -148,7 +148,6 @@ export const SettingsDialog = ({ { - flushPendingNodeColorWrites(); refreshConfigTree(); onClose?.(); }} From 2cfa982b6fbd153dee5d74b332371b866fba86f2 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 22 Jun 2026 01:50:40 -0600 Subject: [PATCH 4/4] Update debounce timing for color writes in NodeConfig component from 250ms to 150ms --- apps/roam/src/components/settings/NodeConfig.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index 8357af740..92e68d5dd 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -45,7 +45,7 @@ export const getCleanTagText = (tag: string): string => { return tag.replace(/^#+/, "").trim().toUpperCase(); }; -const COLOR_WRITE_DEBOUNCE_MS = 250; +const COLOR_WRITE_DEBOUNCE_MS = 150; type DiscourseNodeColorSettingProps = { canvasUid: string;