diff --git a/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx b/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx index e543966c3f..b1b3ae4fa0 100644 --- a/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx @@ -8,9 +8,10 @@ import { Tooltip, } from "@blueprintjs/core"; import React, { useState } from "react"; -import getDiscourseNodes from "~/utils/getDiscourseNodes"; +import getDiscourseNodes, { + excludeDefaultNodes, +} from "~/utils/getDiscourseNodes"; import refreshConfigTree from "~/utils/refreshConfigTree"; -import type { CustomField } from "roamjs-components/components/ConfigPanels/types"; import posthog from "posthog-js"; import getDiscourseRelations, { type DiscourseRelation, @@ -24,20 +25,12 @@ import { } from "./utils/accessors"; import { GLOBAL_KEYS } from "./utils/settingKeys"; import { invalidateDiscourseNodeTypeCaches } from "~/utils/discourseNodeTypeCache"; +import { useSettingsNav } from "./navigation/SettingsNavContext"; -type DiscourseNodeConfigPanelProps = React.ComponentProps< - CustomField["options"]["component"] -> & { - isPopup?: boolean; - setSelectedTabId: (id: string) => void; -}; - -const DiscourseNodeConfigPanel: React.FC = ({ - isPopup, - setSelectedTabId, -}) => { +const DiscourseNodeConfigPanel: React.FC = () => { + const { push } = useSettingsNav(); const [nodes, setNodes] = useState(() => - getDiscourseNodes().filter((n) => n.backedBy === "user"), + getDiscourseNodes().filter(excludeDefaultNodes), ); const [label, setLabel] = useState(""); const [isCreating, setIsCreating] = useState(false); @@ -52,11 +45,8 @@ const DiscourseNodeConfigPanel: React.FC = ({ >([]); const [nodeTypeIdToDelete, setNodeTypeIdToDelete] = useState(""); const navigateToNode = (uid: string) => { - if (isPopup) { - setSelectedTabId(uid); - } else { - window.roamAlphaAPI.ui.mainWindow.openPage({ page: { uid } }); - } + push(uid); + posthog.capture("Settings: Node Type Opened", { nodeTypeUid: uid }); }; const createNodeType = async (): Promise => { diff --git a/apps/roam/src/components/settings/GrammarNodesRoute.tsx b/apps/roam/src/components/settings/GrammarNodesRoute.tsx new file mode 100644 index 0000000000..d480a1ec13 --- /dev/null +++ b/apps/roam/src/components/settings/GrammarNodesRoute.tsx @@ -0,0 +1,55 @@ +import React, { useEffect } from "react"; +import { OnloadArgs } from "roamjs-components/types"; +import getDiscourseNodes, { + excludeDefaultNodes, +} from "~/utils/getDiscourseNodes"; +import { useSettingsNav } from "./navigation/SettingsNavContext"; +import SettingsPageHeader from "./navigation/SettingsPageHeader"; +import DiscourseNodeConfigPanel from "./DiscourseNodeConfigPanel"; +import NodeConfig from "./NodeConfig"; + +const NODES_ANCESTOR_LABELS = ["Grammar"] as const; + +const GrammarNodesRoute = ({ + onloadArgs, +}: { + onloadArgs: OnloadArgs; +}): JSX.Element => { + const { segments, goToDepth } = useSettingsNav(); + const nodes = getDiscourseNodes().filter(excludeDefaultNodes); + + const [nodeTypeUid] = segments; + const node = nodeTypeUid + ? nodes.find((n) => n.type === nodeTypeUid) + : undefined; + + // A deleted node type or stale deep link resolves to nothing; return to the list. + const isStalePath = Boolean(nodeTypeUid) && !node; + useEffect(() => { + if (isStalePath) goToDepth(0); + }, [isStalePath, goToDepth]); + + const resolveLabel = (segment: string): string => + nodes.find((n) => n.type === segment)?.text ?? segment; + + return ( +
+ +
+ {node ? ( + + ) : ( +
+ +
+ )} +
+
+ ); +}; + +export default GrammarNodesRoute; diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 1355aebf6c..9bb230270d 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -1,4 +1,11 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from "react"; import { OnloadArgs } from "roamjs-components/types"; import { Classes, @@ -16,11 +23,6 @@ import discourseConfigRef from "~/utils/discourseConfigRef"; import DiscourseGraphExport from "./ExportSettings"; import QuerySettings from "./QuerySettings"; import AdminPanel from "./AdminPanel"; -import DiscourseNodeConfigPanel from "./DiscourseNodeConfigPanel"; -import getDiscourseNodes, { - excludeDefaultNodes, -} from "~/utils/getDiscourseNodes"; -import NodeConfig from "./NodeConfig"; import PreferencesGeneral from "./PreferencesGeneral"; import PreferencesStyling from "./PreferencesStyling"; import LeftSidebarSettings from "./LeftSidebarSettings"; @@ -32,7 +34,14 @@ import { getVersionWithDate } from "~/utils/getVersion"; import posthog from "posthog-js"; import { bulkReadSettings } from "./utils/accessors"; import { onSettingChange, settingKeys } from "./utils/settingsEmitter"; -import { SETTINGS_TAB_IDS, resolveSettingsTabId } from "./utils/settingsTabs"; +import { SETTINGS_TAB_IDS } from "./utils/settingsTabs"; +import { + resolveInitialSettingsPath, + settingsNavReducer, + tabIdOf, +} from "./utils/settingsNavigation"; +import { SettingsNavProvider } from "./navigation/SettingsNavContext"; +import GrammarNodesRoute from "./GrammarNodesRoute"; const SectionHeader = ({ children }: { children: React.ReactNode }) => (
@@ -77,10 +86,15 @@ export const SettingsDialog = ({ const relationsNode = grammarNode?.children.find( (node) => node.text === "relations", ); - const nodesNode = grammarNode?.children.find((node) => node.text === "nodes"); - const nodes = getDiscourseNodes().filter(excludeDefaultNodes); - const [activeTabId, setActiveTabId] = useState(() => - resolveSettingsTabId(selectedTabId), + const [path, dispatch] = useReducer( + settingsNavReducer, + selectedTabId, + resolveInitialSettingsPath, + ); + const activeTabId = tabIdOf(path); + const selectTab = useCallback( + (tabId: string) => dispatch({ type: "select-tab", tabId }), + [], ); // eslint-disable-next-line react-hooks/exhaustive-deps const settings = useMemo(() => bulkReadSettings(), [activeTabId]); @@ -103,15 +117,14 @@ export const SettingsDialog = ({ const { versionStamp } = getVersionWithDate(); const openAdminPanel = (): void => { setShowAdminPanel(true); - setActiveTabId(SETTINGS_TAB_IDS.admin); + selectTab(SETTINGS_TAB_IDS.admin); posthog.capture("Settings: Admin Panel Opened from Footer"); }; + const initialTabId = useRef(activeTabId).current; useEffect(() => { - posthog.capture("Settings: Dialog Opened", { - initialTabId: String(resolveSettingsTabId(selectedTabId)), - }); - }, [selectedTabId]); + posthog.capture("Settings: Dialog Opened", { initialTabId }); + }, [initialTabId]); useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { @@ -119,14 +132,14 @@ export const SettingsDialog = ({ e.stopPropagation(); e.preventDefault(); setShowAdminPanel(true); - setActiveTabId(SETTINGS_TAB_IDS.admin); + selectTab(SETTINGS_TAB_IDS.admin); posthog.capture("Settings: Admin Panel Opened via Shortcut"); } }; window.addEventListener("keydown", handleKeyPress); return () => window.removeEventListener("keydown", handleKeyPress); - }, []); + }, [selectTab]); return ( { - setActiveTabId(id); + selectTab(String(id)); posthog.capture("Settings: Tab Opened", { tabId: String(id), }); @@ -243,16 +256,10 @@ export const SettingsDialog = ({ + + + } /> } /> - Node types - {nodes.map((n) => ( - } - /> - ))} Advanced & { + title?: string; uid: string; defaultValue?: InputTextNode[]; }; @@ -191,10 +192,12 @@ const DualWriteBlocksPanel = ({ return ( <> - + {title ? ( + + ) : null}