diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index 0f1c350fb..495903500 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -9,14 +9,13 @@ import { ProgressBar, Toaster, Toast, - Tooltip, Tab, Tabs, 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 +90,14 @@ 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 { flushPendingSettingWrites } from "~/utils/pendingSettingWrites"; +import ExportOptions from "./ExportOptions"; +import Description from "~/components/settings/SettingsDescription"; const ExportProgress = ({ id }: { id: string }) => { const [progress, setProgress] = useState(0); @@ -225,6 +231,21 @@ 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); + // 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], + ); + + // 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(); + }; const [gitHubAccessToken, setGitHubAccessToken] = useState( getSetting("oauth-github", null), ); @@ -756,7 +777,7 @@ const ExportDialog: ExportDialogComponent = ({ }); } finally { setLoading(false); - onClose(); + closeDialog(); } }; @@ -796,7 +817,7 @@ const ExportDialog: ExportDialogComponent = ({ fileCount: files.length, }); } - onClose(); + closeDialog(); } catch (e) { setError("Failed to export files."); posthog.capture("Export Dialog: Export Failed", { @@ -888,7 +909,7 @@ const ExportDialog: ExportDialogComponent = ({ : "success", id: "query-builder-publish-success", }); - if (hasPublishedNodes) onClose(); + if (hasPublishedNodes) closeDialog(); } catch (e) { internalError({ error: e as Error, @@ -951,41 +972,53 @@ const ExportDialog: ExportDialogComponent = ({ /> -
+
+
+
{error} -
-
); }; diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx index 67a546c92..5af703692 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -1,5 +1,5 @@ import React, { useState, useMemo } from "react"; -import { Button, Intent } from "@blueprintjs/core"; +import { Button, Collapse, Intent } from "@blueprintjs/core"; import DualWriteBlocksPanel from "./components/EphemeralBlocksPanel"; import getSubTree from "roamjs-components/util/getSubTree"; import { DiscourseNode } from "~/utils/getDiscourseNodes"; @@ -17,6 +17,7 @@ import { TEMPLATE_SETTING_KEYS, } from "~/components/settings/utils/settingKeys"; import { RenderRoamBlock } from "~/utils/roamReactComponents"; +import Description from "~/components/settings/SettingsDescription"; import { ROAM_DOCS, withDocsLink } from "./utils/docs"; const DiscourseNodeSuggestiveRules = ({ @@ -42,6 +43,7 @@ const DiscourseNodeSuggestiveRules = ({ }).uid || "", [nodeUid], ); + const [isTemplateOpen, setIsTemplateOpen] = useState(false); const [isUpdating, setIsUpdating] = useState(false); @@ -68,18 +70,39 @@ const DiscourseNodeSuggestiveRules = ({ }; return ( -
- +
+
+
+
+ {/* Collapse unmounts its children, so the buffer block only exists while open. */} + +
+ +
+
+
= { + [nodeConfigSegmentIds.index]: "Index", + [nodeConfigSegmentIds.template]: "Template", +}; + const GrammarNodesRoute = ({ onloadArgs, }: { @@ -18,7 +26,7 @@ const GrammarNodesRoute = ({ const { segments, goToDepth } = useSettingsNav(); const nodes = getDiscourseNodes().filter(excludeDefaultNodes); - const [nodeTypeUid] = segments; + const [nodeTypeUid, subPage] = segments; const node = nodeTypeUid ? nodes.find((n) => n.type === nodeTypeUid) : undefined; @@ -29,8 +37,10 @@ const GrammarNodesRoute = ({ if (isStalePath) goToDepth(0); }, [isStalePath, goToDepth]); - const resolveLabel = (segment: string): string => - nodes.find((n) => n.type === segment)?.text ?? segment; + const resolveLabel = (segment: string, segmentIndex: number): string => + segmentIndex === 0 + ? (nodes.find((n) => n.type === segment)?.text ?? segment) + : (SUB_PAGE_LABELS[segment] ?? segment); return (
@@ -40,12 +50,16 @@ const GrammarNodesRoute = ({ resolveLabel={resolveLabel} />
- {node ? ( - - ) : ( + {!node ? (
+ ) : subPage === nodeConfigSegmentIds.index ? ( + + ) : subPage === nodeConfigSegmentIds.template ? ( + + ) : ( + )}
diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index b4114b63b..4f0672a86 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -1,13 +1,9 @@ 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"; import Description from "~/components/settings/SettingsDescription"; import { Label, - Tabs, - Tab, - TabId, InputGroup, ControlGroup, Tooltip, @@ -18,8 +14,6 @@ import DiscourseNodeAttributes from "./DiscourseNodeAttributes"; import DiscourseNodeCanvasSettings, { formatHexColor, } from "./DiscourseNodeCanvasSettings"; -import DiscourseNodeIndex from "./DiscourseNodeIndex"; -import { OnloadArgs } from "roamjs-components/types"; import setInputSetting from "roamjs-components/util/setInputSetting"; import { getDiscourseNodeSetting, @@ -30,7 +24,6 @@ import { CANVAS_KEYS, DISCOURSE_NODE_KEYS, SPECIFICATION_KEYS, - TEMPLATE_SETTING_KEYS, } from "~/components/settings/utils/settingKeys"; import DiscourseNodeSuggestiveRules from "./DiscourseNodeSuggestiveRules"; import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors"; @@ -40,6 +33,10 @@ import { DiscourseNodeSelectPanel, } from "./components/BlockPropSettingPanels"; import { ROAM_DOCS, withDocsLink } from "./utils/docs"; +import { SettingsGroup } from "./components/SettingsHeadings"; +import SettingsDrillDownRow from "./components/SettingsDrillDownRow"; +import { useSettingsNav } from "./navigation/SettingsNavContext"; +import { nodeConfigSegmentIds } from "./utils/settingsNavigation"; export const getCleanTagText = (tag: string): string => { return tag.replace(/^#+/, "").trim().toUpperCase(); @@ -168,13 +165,7 @@ const generateTagPlaceholder = (node: DiscourseNode): string => { return `#${nodeTextPrefix}-candidate`; // Evidence = #evi-candidate }; -const NodeConfig = ({ - node, - onloadArgs, -}: { - node: DiscourseNode; - onloadArgs: OnloadArgs; -}) => { +const NodeConfig = ({ node }: { node: DiscourseNode }) => { const getUid = (key: string) => getSubTree({ parentUid: node.type, @@ -184,19 +175,17 @@ const NodeConfig = ({ 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 nav = useSettingsNav(); const [tagError, setTagError] = useState(""); const [formatError, setFormatError] = useState(""); const [shortcutError, setShortcutError] = useState(""); @@ -302,220 +291,169 @@ const NodeConfig = ({ ); return ( - <> - setSelectedTabId(id)} - selectedTabId={selectedTabId} - renderActiveTabPanelOnly={true} - > - - - -
- -
- -
- } - /> - + + - -
- } + description={`The saved list of all ${node.text} pages \u2014 which pages appear and which columns show.`} + buttonText={`See all ${node.text} nodes`} + onClick={() => nav.push(nodeConfigSegmentIds.index)} + /> + + + - - - -
- } + description={withDocsLink( + `The format ${node.text} pages should have.`, + ROAM_DOCS.grammarNodes, + )} + settingKeys={[DISCOURSE_NODE_KEYS.format]} + initialValue={node.format} + error={formatError} + onChange={setFormatValue} + order={3} + parentUid={node.type} + uid={formatUid} + /> + + + + - - -
- } + description={withDocsLink( + `The template that auto fills ${node.text} page when generated.`, + ROAM_DOCS.creatingNodes, + )} + buttonText="Edit template" + onClick={() => nav.push(nodeConfigSegmentIds.template)} /> - - >( - node.type, - [DISCOURSE_NODE_KEYS.attributes], - )} - /> - c.text)} - initialValue={ - getDiscourseNodeSetting(node.type, [ - DISCOURSE_NODE_KEYS.overlay, - ]) ?? "" - } - order={0} - parentUid={node.type} - uid={overlayUid} - /> - - } + + + + + + + + + + + + {isSyncEnabled() && ( + + + + )} + + + >( + node.type, + [DISCOURSE_NODE_KEYS.attributes], + )} /> - - - - + c.text)} + initialValue={ + getDiscourseNodeSetting(node.type, [ + DISCOURSE_NODE_KEYS.overlay, + ]) ?? "" } + order={0} + parentUid={node.type} + uid={overlayUid} /> - {isSyncEnabled() && ( - - - - } - /> - )} - - + + ); }; diff --git a/apps/roam/src/components/settings/NodeIndexPage.tsx b/apps/roam/src/components/settings/NodeIndexPage.tsx new file mode 100644 index 000000000..c39558a82 --- /dev/null +++ b/apps/roam/src/components/settings/NodeIndexPage.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { getSubTree } from "roamjs-components/util"; +import { OnloadArgs } from "roamjs-components/types"; +import { DiscourseNode } from "~/utils/getDiscourseNodes"; +import DiscourseNodeIndex from "./DiscourseNodeIndex"; + +const NodeIndexPage = ({ + node, + onloadArgs, +}: { + node: DiscourseNode; + onloadArgs: OnloadArgs; +}): JSX.Element => { + const indexUid = getSubTree({ parentUid: node.type, key: "Index" }).uid; + return ( +
+ +
+ ); +}; + +export default NodeIndexPage; diff --git a/apps/roam/src/components/settings/NodeTemplatePage.tsx b/apps/roam/src/components/settings/NodeTemplatePage.tsx new file mode 100644 index 000000000..94947976d --- /dev/null +++ b/apps/roam/src/components/settings/NodeTemplatePage.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { getSubTree } from "roamjs-components/util"; +import { DiscourseNode } from "~/utils/getDiscourseNodes"; +import DualWriteBlocksPanel from "./components/EphemeralBlocksPanel"; +import { TEMPLATE_SETTING_KEYS } from "~/components/settings/utils/settingKeys"; +import { ROAM_DOCS, withDocsLink } from "./utils/docs"; + +const NodeTemplatePage = ({ node }: { node: DiscourseNode }): JSX.Element => { + const templateUid = getSubTree({ parentUid: node.type, key: "Template" }).uid; + return ( +
+ +
+ ); +}; + +export default NodeTemplatePage; diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index fcba1a38e..138c3bcfa 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -20,7 +20,6 @@ import renderOverlay from "roamjs-components/util/renderOverlay"; import DiscourseRelationConfigPanel from "./DiscourseRelationConfigPanel"; import DEFAULT_RELATION_VALUES from "~/data/defaultDiscourseRelations"; import discourseConfigRef from "~/utils/discourseConfigRef"; -import DiscourseGraphExport from "./ExportSettings"; import QuerySettings from "./QuerySettings"; import AdminPanel from "./AdminPanel"; import PreferencesGeneral from "./PreferencesGeneral"; @@ -282,14 +281,6 @@ export const SettingsDialog = ({ /> } /> - - } - /> {/* Secret Admin Panel */} void; +}): JSX.Element => ( + +); + +export default SettingsDrillDownRow; diff --git a/apps/roam/src/components/settings/utils/settingsNavigation.ts b/apps/roam/src/components/settings/utils/settingsNavigation.ts index 2d27b08da..bcef6a444 100644 --- a/apps/roam/src/components/settings/utils/settingsNavigation.ts +++ b/apps/roam/src/components/settings/utils/settingsNavigation.ts @@ -14,6 +14,11 @@ export type SettingsNavAction = | { type: "pop" } | { type: "truncate"; depth: number }; +export const nodeConfigSegmentIds = { + index: "index", + template: "template", +} as const; + export const rootPath = (tabId: string): SettingsPath => [tabId]; export const tabIdOf = (path: SettingsPath): string => path[0] ?? ""; diff --git a/apps/roam/src/components/settings/utils/settingsTabs.ts b/apps/roam/src/components/settings/utils/settingsTabs.ts index 2b4e0a05b..0c0e8e163 100644 --- a/apps/roam/src/components/settings/utils/settingsTabs.ts +++ b/apps/roam/src/components/settings/utils/settingsTabs.ts @@ -9,7 +9,6 @@ export const SETTINGS_TAB_IDS = { grammarNodes: "grammar-nodes", grammarRelations: "grammar-relations", advancedQueries: "advanced-queries", - advancedExport: "advanced-export", admin: "secret-admin-panel", } as const; @@ -24,7 +23,8 @@ 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, - "discourse-graph-export": SETTINGS_TAB_IDS.advancedExport, + // 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, }; diff --git a/apps/roam/src/styles/settingsStyles.css b/apps/roam/src/styles/settingsStyles.css index 4a76d35a3..c6d345e00 100644 --- a/apps/roam/src/styles/settingsStyles.css +++ b/apps/roam/src/styles/settingsStyles.css @@ -81,3 +81,10 @@ .dg-settings-page-header__crumb { cursor: pointer; } + +.dg-settings-node-page { + display: flex; + flex-direction: column; + gap: 44px; + padding: 2px; +} diff --git a/apps/website/content/roam/guides/sharing-discourse-graph.md b/apps/website/content/roam/guides/sharing-discourse-graph.md index be5d44b17..b110584e6 100644 --- a/apps/website/content/roam/guides/sharing-discourse-graph.md +++ b/apps/website/content/roam/guides/sharing-discourse-graph.md @@ -17,9 +17,7 @@ Demo: ## Export Options -We have a range of options for customizing the markdown export. These can be found on the `Export` tab of the discourse graph configuration. - -![](/docs/roam/settings-export.png) +We have a range of options for customizing the markdown export. These can be found under `Export options` on the `Export` tab of the export dialog, alongside the export you are about to run. Here is a brief explanation of each option: diff --git a/apps/website/public/docs/roam/settings-export.png b/apps/website/public/docs/roam/settings-export.png deleted file mode 100644 index 02291fc57..000000000 Binary files a/apps/website/public/docs/roam/settings-export.png and /dev/null differ