Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 9 additions & 19 deletions apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<DiscourseNodeConfigPanelProps> = ({
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);
Expand All @@ -52,11 +45,8 @@ const DiscourseNodeConfigPanel: React.FC<DiscourseNodeConfigPanelProps> = ({
>([]);
const [nodeTypeIdToDelete, setNodeTypeIdToDelete] = useState<string>("");
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<void> => {
Expand Down
55 changes: 55 additions & 0 deletions apps/roam/src/components/settings/GrammarNodesRoute.tsx
Original file line number Diff line number Diff line change
@@ -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);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path outlives this panel — renderActiveTabPanelOnly unmounts and remounts it on every tab switch — so it can point at a node type deleted in the meantime, or arrive stale from a saved link. goToDepth(0) rather than pop() so a depth-2 stale path converges in one dispatch instead of two.

}, [isStalePath, goToDepth]);

const resolveLabel = (segment: string): string =>
nodes.find((n) => n.type === segment)?.text ?? segment;

return (
<div className="dg-settings-route">
<SettingsPageHeader
ancestorLabels={NODES_ANCESTOR_LABELS}
rootLabel="Nodes"
resolveLabel={resolveLabel}
/>
<div className="dg-settings-route__body">
{node ? (
<NodeConfig node={node} onloadArgs={onloadArgs} />
) : (
<div className="p-1">
<DiscourseNodeConfigPanel />
</div>
)}
</div>
</div>
);
};

export default GrammarNodesRoute;
10 changes: 10 additions & 0 deletions apps/roam/src/components/settings/NodeConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,18 @@ const DiscourseNodeColorSetting = ({
[canvasUid, nodeType],
);

// Navigating away unmounts mid-debounce, so the pending colour is written rather than dropped.
const pendingColorRef = useRef<string | null>(null);
const persistColorValueRef = useRef(persistColorValue);
persistColorValueRef.current = persistColorValue;
useEffect(() => {
return () => {
if (!colorWriteTimeoutRef.current) return;

window.clearTimeout(colorWriteTimeoutRef.current);
const pending = pendingColorRef.current;
pendingColorRef.current = null;
if (pending !== null) persistColorValueRef.current(pending);
};
}, []);

Expand All @@ -94,8 +101,10 @@ const DiscourseNodeColorSetting = ({
window.clearTimeout(colorWriteTimeoutRef.current);
colorWriteTimeoutRef.current = null;
}
pendingColorRef.current = colorValue;
colorWriteTimeoutRef.current = window.setTimeout(() => {
persistColorValue(colorValue);
pendingColorRef.current = null;
colorWriteTimeoutRef.current = null;
}, COLOR_WRITE_DEBOUNCE_MS);
};
Expand Down Expand Up @@ -134,6 +143,7 @@ const DiscourseNodeColorSetting = ({
window.clearTimeout(colorWriteTimeoutRef.current);
colorWriteTimeoutRef.current = null;
}
pendingColorRef.current = null;
setColor("");
persistColorValue("");
}}
Expand Down
73 changes: 35 additions & 38 deletions apps/roam/src/components/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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";
Expand All @@ -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 }) => (
<div className="bp3-tab-copy mt-4 cursor-default select-none text-lg font-semibold text-neutral-dark">
Expand Down Expand Up @@ -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<TabId>(() =>
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]);
Expand All @@ -98,30 +112,29 @@ 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) => {
if (e.ctrlKey && e.shiftKey && e.key === "A") {
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 (
<Dialog
isOpen={isOpen}
Expand Down Expand Up @@ -164,7 +177,7 @@ export const SettingsDialog = ({
<Tabs
className="dg-settings-tabs flex h-full"
onChange={(id) => {
setActiveTabId(id);
selectTab(String(id));
posthog.capture("Settings: Tab Opened", {
tabId: String(id),
});
Expand Down Expand Up @@ -238,16 +251,10 @@ export const SettingsDialog = ({
<Tab
id={SETTINGS_TAB_IDS.grammarNodes}
title="Nodes"
className="overflow-y-auto"
panel={
<DiscourseNodeConfigPanel
title="Nodes"
uid={nodesNode?.uid || ""}
parentUid={grammarNode?.uid || ""}
defaultValue={[]}
setSelectedTabId={setActiveTabId}
isPopup={true}
/>
<SettingsNavProvider path={path} dispatch={dispatch}>
<GrammarNodesRoute onloadArgs={onloadArgs} />
</SettingsNavProvider>
}
/>
<Tab
Expand All @@ -263,16 +270,6 @@ export const SettingsDialog = ({
/>
}
/>
<SectionHeader>Node types</SectionHeader>
{nodes.map((n) => (
<Tab
key={n.type}
id={n.type}
title={n.text}
className="overflow-y-auto"
panel={<NodeConfig node={n} onloadArgs={onloadArgs} />}
/>
))}
<SectionHeader>Advanced</SectionHeader>
<Tab
id={SETTINGS_TAB_IDS.advancedQueries}
Expand Down
Loading