From e1d925d8b9dfb80d184c442b01eb45b24d221d91 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 31 Aug 2026 11:27:14 -0400 Subject: [PATCH 01/10] ENG-2185 Move export settings out of Settings into the Export panel The 7 export controls were used by exactly one surface but lived in Settings, so changing an option meant leaving the Export flow, opening Settings, and coming back. Move them into a collapsed "Export options" disclosure at the bottom of the Export dialog's Export tab, and remove the Advanced > Export tab that ENG-2186 parked in Settings for this change to take out. The controls, their storage keys, and the read path in getExportSettings are unchanged, so saved values persist and export behaviour is untouched. The advancedExport tab id goes with it, and the saved "discourse-graph-export" deep link now resolves to the default tab instead of a tab that no longer renders. Co-Authored-By: Claude Opus 5 --- apps/roam/src/components/Export.tsx | 66 +++++++++++++++---- .../ExportSettings.tsx => ExportOptions.tsx} | 10 +-- .../roam/src/components/settings/Settings.tsx | 10 --- .../components/settings/utils/settingsTabs.ts | 4 +- .../roam/guides/sharing-discourse-graph.md | 2 +- 5 files changed, 63 insertions(+), 29 deletions(-) rename apps/roam/src/components/{settings/ExportSettings.tsx => ExportOptions.tsx} (94%) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index 0f1c350fb..4941b4dd6 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -15,8 +15,9 @@ import { RadioGroup, Radio, FormGroup, + Collapse, } from "@blueprintjs/core"; -import React, { useState, useEffect, useMemo, FormEvent } from "react"; +import React, { useState, useEffect, useMemo, useRef, FormEvent } from "react"; import MenuItemSelect from "roamjs-components/components/MenuItemSelect"; import { saveAs } from "file-saver"; import { Result } from "roamjs-components/types/query-builder"; @@ -91,7 +92,12 @@ import { type NodeUidWithType, } from "~/utils/publishNodesToGroups"; import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext"; -import { isNodeSharingEnabled } from "~/components/settings/utils/accessors"; +import { + bulkReadSettings, + isNodeSharingEnabled, +} from "~/components/settings/utils/accessors"; +import refreshConfigTree from "~/utils/refreshConfigTree"; +import ExportOptions from "./ExportOptions"; const ExportProgress = ({ id }: { id: string }) => { const [progress, setProgress] = useState(0); @@ -225,6 +231,22 @@ const ExportDialog: ExportDialogComponent = ({ if (initialPanel) setSelectedTabId(INITIAL_PANEL_TO_TAB_ID[initialPanel]); }, [initialPanel, sharingEnabled]); const [includeDiscourseContext, setIncludeDiscourseContext] = useState(false); + const [exportOptionsOpen, setExportOptionsOpen] = useState(false); + const exportOptionsOpened = useRef(false); + const exportGlobalSettings = useMemo( + () => bulkReadSettings().globalSettings, + [], + ); + + // The export option panels write legacy config blocks alongside block props, so + // the cached config tree has to be refreshed the way SettingsDialog does. Gated + // on the section having been opened because refreshConfigTree re-reads every + // node page and re-registers the datalog translators, which is too heavy to run + // on every close of a dialog that is opened for each export. + const closeDialog = (): void => { + if (exportOptionsOpened.current) refreshConfigTree(); + onClose(); + }; const [gitHubAccessToken, setGitHubAccessToken] = useState( getSetting("oauth-github", null), ); @@ -756,7 +778,7 @@ const ExportDialog: ExportDialogComponent = ({ }); } finally { setLoading(false); - onClose(); + closeDialog(); } }; @@ -796,7 +818,7 @@ const ExportDialog: ExportDialogComponent = ({ fileCount: files.length, }); } - onClose(); + closeDialog(); } catch (e) { setError("Failed to export files."); posthog.capture("Export Dialog: Export Failed", { @@ -888,7 +910,7 @@ const ExportDialog: ExportDialogComponent = ({ : "success", id: "query-builder-publish-success", }); - if (hasPublishedNodes) onClose(); + if (hasPublishedNodes) closeDialog(); } catch (e) { internalError({ error: e as Error, @@ -981,11 +1003,33 @@ const ExportDialog: ExportDialogComponent = ({ + +
+
{error} -
- -
-
+ +
+ +
+
From a0293d16bf3bb26487b8b9430d16b4764be00ebe Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Sat, 5 Sep 2026 19:12:07 -0400 Subject: [PATCH 05/10] ENG-2185 Discourse context on its own line above the export options row Review feedback: the checkbox sits right-aligned on the line above; the line below is the Export options toggle on the left and the result count on the right. Co-Authored-By: Claude Fable 5.1 --- apps/roam/src/components/Export.tsx | 58 ++++++++++++++--------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index 6d76d657b..e8ab30a57 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -978,7 +978,30 @@ const ExportDialog: ExportDialogComponent = ({ /> -
+
+ + { + setIncludeDiscourseContext( + (e.target as HTMLInputElement).checked, + ); + }} + labelElement={ + + Discourse context + + } + /> + +
+
From af1430faf7091b866fa12880dfe9b7b454f617de Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 31 Aug 2026 13:50:40 -0400 Subject: [PATCH 06/10] ENG-2183 Build the new setting-item row component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add one shared settings row — scope indicator, label, always-visible description, control — and route every panel-based row through it. Descriptions move out of the hover popover that dismissed before its doc links could be clicked (ENG-2080). The fix is structural: the description renders as a sibling of the label, never a descendant, so a link inside it can no longer toggle the row's control. The rewire happens at SettingTitle, the one helper all five Base*Panels funnelled label and description through, so ~70 call sites are unchanged. Flag rows swap Checkbox for Switch to match the design. Scope is derived from the wrapper rather than passed per call site, because the wrapper already binds the setter that decides who a value is written for. Rows that cannot derive it pass `scope` explicitly. Layout is Tailwind, which resolves because Roam itself bundles Tailwind. Anything depending on Tailwind's preflight is hand-rolled instead: `border-b` and `border` set only a width, and `* { border-style: solid }` is absent, so those borders never paint. This repo's own `neutral-*` theme colours are likewise absent from Roam's build and are mirrored as CSS variables. Supersedes ENG-2186's SettingsDrillDownRow, whose two callers now compose the shared row with a trailing drill-down summary. Co-Authored-By: Claude Opus 5 --- .../roam/src/components/DiscourseNodeMenu.tsx | 5 +- .../components/DiscourseNodeSearchMenu.tsx | 4 +- .../settings/KeyboardShortcutInput.tsx | 58 ++-- .../src/components/settings/NodeConfig.tsx | 143 +++++---- .../components/settings/PageGroupPanel.tsx | 198 ++++++------ .../settings/PreferencesGeneral.tsx | 57 ++-- .../src/components/settings/QuerySettings.tsx | 35 +-- .../settings/SettingsDescription.tsx | 19 +- .../components/BlockPropSettingPanels.tsx | 291 +++++++++++------- .../components/EphemeralBlocksPanel.tsx | 32 +- .../components/SettingDrillDownSummary.tsx | 33 ++ .../settings/components/SettingItemRow.tsx | 112 +++++++ .../components/SettingKeycapInput.tsx | 21 ++ .../components/SettingsDrillDownRow.tsx | 31 -- apps/roam/src/styles/settingsStyles.css | 29 ++ 15 files changed, 674 insertions(+), 394 deletions(-) create mode 100644 apps/roam/src/components/settings/components/SettingDrillDownSummary.tsx create mode 100644 apps/roam/src/components/settings/components/SettingItemRow.tsx create mode 100644 apps/roam/src/components/settings/components/SettingKeycapInput.tsx delete mode 100644 apps/roam/src/components/settings/components/SettingsDrillDownRow.tsx diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index 4ae83f584..7a90f7819 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -4,11 +4,11 @@ import { Popover, Position, Button, - InputGroup, getKeyCombo, IKeyCombo, Icon, } from "@blueprintjs/core"; +import SettingKeycapInput from "~/components/settings/components/SettingKeycapInput"; import React, { useCallback, useEffect, @@ -496,7 +496,8 @@ export const NodeMenuTriggerComponent = ({ const shortcut = useMemo(() => comboToString(comboKey), [comboKey]); return ( - - {label} - - setIsActive(true)} - onBlur={() => setIsActive(false)} - rightElement={ -
)} - + + { + if (colorWriteTimeoutRef.current) { + window.clearTimeout(colorWriteTimeoutRef.current); + colorWriteTimeoutRef.current = null; + } + pendingColorRef.current = null; + setColor(""); + persistColorValue(""); + }} + /> + + + } + /> ); }; @@ -293,11 +290,16 @@ const NodeConfig = ({ node }: { node: DiscourseNode }) => { return (
- nav.push(nodeConfigSegmentIds.index)} + scope="nodeType" + control={ + nav.push(nodeConfigSegmentIds.index)} + /> + } /> { parentUid={node.type} uid={shortcutUid} /> - nav.push(nodeConfigSegmentIds.template)} + scope="nodeType" + control={ + nav.push(nodeConfigSegmentIds.template)} + /> + } /> @@ -396,26 +403,28 @@ const NodeConfig = ({ node }: { node: DiscourseNode }) => { {/* Settings mid-migration live here until they either replace their predecessor or are removed. */} - + { + validate({ + tag: tagValue, + format: formatValue, + isSpecificationEnabled, + }); + }} + /> + } + /> {isSyncEnabled() && ( diff --git a/apps/roam/src/components/settings/PageGroupPanel.tsx b/apps/roam/src/components/settings/PageGroupPanel.tsx index 584cb83b5..c68bf84fc 100644 --- a/apps/roam/src/components/settings/PageGroupPanel.tsx +++ b/apps/roam/src/components/settings/PageGroupPanel.tsx @@ -1,12 +1,12 @@ import React, { useState, useCallback } from "react"; -import { Label, Button, Intent, Tag, InputGroup } from "@blueprintjs/core"; -import Description from "~/components/settings/SettingsDescription"; +import { Button, Intent, Tag, InputGroup } from "@blueprintjs/core"; import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; import createBlock from "roamjs-components/writes/createBlock"; import deleteBlock from "roamjs-components/writes/deleteBlock"; import getAllPageNames from "roamjs-components/queries/getAllPageNames"; import { type PageGroup } from "~/utils/getSuggestiveModeConfigSettings"; import { setGlobalSetting } from "~/components/settings/utils/accessors"; +import SettingItemRow from "./components/SettingItemRow"; import { GLOBAL_KEYS, SUGGESTIVE_MODE_KEYS, @@ -129,107 +129,109 @@ const PageGroupsPanel = ({ }, []); return ( -