From c38e3f4645f46934823f7bdb5c93912d079859a0 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 31 Aug 2026 11:27:14 -0400 Subject: [PATCH 1/8] 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 | 9 --- .../components/settings/utils/settingsTabs.ts | 4 +- .../roam/guides/sharing-discourse-graph.md | 2 +- 5 files changed, 63 insertions(+), 28 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 daa5a1040d..ea08b0f2cd 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"; @@ -93,7 +94,12 @@ import { } from "~/utils/publishNodesToGroups"; import { summarizeAssetResults } from "~/utils/publishNodeAssets"; 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); @@ -227,6 +233,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), ); @@ -758,7 +780,7 @@ const ExportDialog: ExportDialogComponent = ({ }); } finally { setLoading(false); - onClose(); + closeDialog(); } }; @@ -798,7 +820,7 @@ const ExportDialog: ExportDialogComponent = ({ fileCount: files.length, }); } - onClose(); + closeDialog(); } catch (e) { setError("Failed to export files."); posthog.capture("Export Dialog: Export Failed", { @@ -941,7 +963,7 @@ const ExportDialog: ExportDialogComponent = ({ : "success", id: "query-builder-publish-success", }); - if (hasPublishedNodes) onClose(); + if (hasPublishedNodes) closeDialog(); } catch (e) { internalError({ error: e as Error, @@ -1034,11 +1056,33 @@ const ExportDialog: ExportDialogComponent = ({ + +
+
{error} -
- -
-
+ +
+ +
+
From b0af22cdf76d99a9cc1ee6e06a38cad0b3370b27 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Sat, 5 Sep 2026 19:12:07 -0400 Subject: [PATCH 5/8] 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 35143e5bf6..cf37f19a77 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -1031,7 +1031,30 @@ const ExportDialog: ExportDialogComponent = ({ /> -
+
+ + { + setIncludeDiscourseContext( + (e.target as HTMLInputElement).checked, + ); + }} + labelElement={ + + Discourse context + + } + /> + +
+
From 9e33be048db945327ec433e34675f2d465efc604 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 7 Sep 2026 18:20:13 -0400 Subject: [PATCH 6/8] ENG-2185 Drop unit tests and trim comments to the decisions they record Co-Authored-By: Claude Fable 5.1 --- apps/roam/src/components/Export.tsx | 18 +++++------------- .../components/settings/utils/settingsTabs.ts | 2 +- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index cf37f19a77..449b2c1c7f 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -236,20 +236,15 @@ const ExportDialog: ExportDialogComponent = ({ const [includeDiscourseContext, setIncludeDiscourseContext] = useState(false); const [exportOptionsOpen, setExportOptionsOpen] = useState(false); const exportOptionsOpened = useRef(false); - // Re-read on every open rather than once at mount: Collapse unmounts the option - // panels while closed, so each open seeds them from the current stored values - // instead of whatever the dialog saw when it first rendered. + // Collapse unmounts the panels while closed, so each open seeds them from current values. const exportGlobalSettings = useMemo( () => bulkReadSettings().globalSettings, // eslint-disable-next-line react-hooks/exhaustive-deps [exportOptionsOpen], ); - // 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. + // Option panels also write legacy config blocks, so refresh the tree as SettingsDialog + // does, but only if they were opened: refreshConfigTree re-reads every node page. const closeDialog = (): void => { if (exportOptionsOpened.current) refreshConfigTree(); onClose(); @@ -1108,11 +1103,8 @@ const ExportDialog: ExportDialogComponent = ({ // eslint-disable-next-line @typescript-eslint/no-misused-promises setTimeout(async () => { try { - // The export reads settings inside its callback, and the number - // and select panels defer their write behind a short timer. The - // await matters as much as the flush: committing only starts the - // Roam block update, so an option edited a moment ago would - // otherwise still read as its previous value here. + // Awaited, not just fired: a commit only starts the Roam update, and an option edited + // a moment ago would otherwise still read as its previous value here. await flushPendingSettingWrites(); const exportType = exportTypes.find( (e) => e.name === activeExportType, diff --git a/apps/roam/src/components/settings/utils/settingsTabs.ts b/apps/roam/src/components/settings/utils/settingsTabs.ts index 351b62773f..61df20dcbc 100644 --- a/apps/roam/src/components/settings/utils/settingsTabs.ts +++ b/apps/roam/src/components/settings/utils/settingsTabs.ts @@ -24,7 +24,7 @@ export const SETTINGS_TAB_ALIASES: Record = { "canvas-shortcuts-personal-settings": SETTINGS_TAB_IDS.featuresCanvas, "left-sidebar-personal-settings": SETTINGS_TAB_IDS.featuresLeftSidebar, "left-sidebar-global-settings": SETTINGS_TAB_IDS.featuresLeftSidebar, - // Export options left Settings in ENG-2185; they now live in the Export dialog. + // Export options now live in the Export dialog. "discourse-graph-export": SETTINGS_TAB_IDS.preferencesGeneral, "discourse-nodes": SETTINGS_TAB_IDS.grammarNodes, "discourse-relations": SETTINGS_TAB_IDS.grammarRelations, From b2baa8a7b8981828e5b13787f0bd717e36448815 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Wed, 9 Sep 2026 15:35:47 -0400 Subject: [PATCH 7/8] ENG-2185 Put Discourse context inside Export options with an info tooltip Co-Authored-By: Claude Fable 5.1 --- apps/roam/src/components/Export.tsx | 44 ++++++++++++----------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index 449b2c1c7f..b6eda423f7 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -9,12 +9,10 @@ import { ProgressBar, Toaster, Toast, - Tooltip, Tab, Tabs, RadioGroup, Radio, - FormGroup, Collapse, } from "@blueprintjs/core"; import React, { useState, useEffect, useMemo, useRef, FormEvent } from "react"; @@ -101,6 +99,7 @@ import { import refreshConfigTree from "~/utils/refreshConfigTree"; import { flushPendingSettingWrites } from "~/utils/pendingSettingWrites"; import ExportOptions from "./ExportOptions"; +import Description from "~/components/settings/SettingsDescription"; const ExportProgress = ({ id }: { id: string }) => { const [progress, setProgress] = useState(0); @@ -1026,30 +1025,7 @@ const ExportDialog: ExportDialogComponent = ({ /> -
- - { - setIncludeDiscourseContext( - (e.target as HTMLInputElement).checked, - ); - }} - labelElement={ - - Discourse context - - } - /> - -
-
+