diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index d73975c2f..f458e80a7 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -27,6 +27,7 @@ import { getNewDiscourseNodeText } from "~/utils/formatUtils"; import { OnloadArgs } from "roamjs-components/types"; import { formatHexColor } from "./settings/DiscourseNodeCanvasSettings"; import posthog from "posthog-js"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; type Props = { textarea?: HTMLTextAreaElement; @@ -406,6 +407,13 @@ export const getModifiersFromCombo = (comboKey: IKeyCombo) => { ].filter(Boolean); }; +export const comboToString = (combo: IKeyCombo): string => { + if (!combo.key) return ""; + const modifiers = getModifiersFromCombo(combo); + const comboString = [...modifiers, combo.key].join("+"); + return normalizeKeyCombo(comboString).join("+"); +}; + export const NodeMenuTriggerComponent = ({ extensionAPI, }: { @@ -427,19 +435,15 @@ export const NodeMenuTriggerComponent = ({ const comboObj = getKeyCombo(e.nativeEvent); if (!comboObj.key) return; - setComboKey({ key: comboObj.key, modifiers: comboObj.modifiers }); - extensionAPI.settings.set("personal-node-menu-trigger", comboObj); + const combo = { key: comboObj.key, modifiers: comboObj.modifiers }; + setComboKey(combo); + void extensionAPI.settings.set("personal-node-menu-trigger", combo); + setPersonalSetting(["Personal node menu trigger"], combo); }, [extensionAPI], ); - const shortcut = useMemo(() => { - if (!comboKey.key) return ""; - - const modifiers = getModifiersFromCombo(comboKey); - const comboString = [...modifiers, comboKey.key].join("+"); - return normalizeKeyCombo(comboString).join("+"); - }, [comboKey]); + const shortcut = useMemo(() => comboToString(comboKey), [comboKey]); return ( { setComboKey({ modifiers: 0, key: "" }); - extensionAPI.settings.set("personal-node-menu-trigger", ""); + void extensionAPI.settings.set("personal-node-menu-trigger", ""); + setPersonalSetting(["Personal node menu trigger"], ""); }} minimal /> diff --git a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx index 021fc005c..abef73705 100644 --- a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx @@ -26,6 +26,7 @@ import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpr import { Result } from "~/utils/types"; import { getSetting } from "~/utils/extensionSettings"; import MiniSearch from "minisearch"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; type Props = { textarea: HTMLTextAreaElement; @@ -724,7 +725,8 @@ export const NodeSearchMenuTriggerSetting = ({ .trim(); setNodeSearchTrigger(trigger); - extensionAPI.settings.set("node-search-trigger", trigger); + void extensionAPI.settings.set("node-search-trigger", trigger); + setPersonalSetting(["Node search menu trigger"], trigger); }; return ( JSX.Element; type Props = Parameters[0]; -const QueryBuilder = ({ pageUid, isEditBlock, showAlias }: Props) => { +const QueryBuilder = ({ + pageUid, + isEditBlock, + showAlias, + discourseNodeType, + settingKey, + returnNode, +}: Props) => { const extensionAPI = useExtensionAPI(); const hideMetadata = useMemo( () => @@ -158,6 +168,9 @@ const QueryBuilder = ({ pageUid, isEditBlock, showAlias }: Props) => { <> { setHasResults(true); setIsEdit(false); diff --git a/apps/roam/src/components/QueryEditor.tsx b/apps/roam/src/components/QueryEditor.tsx index 96900c7bc..fbca18c49 100644 --- a/apps/roam/src/components/QueryEditor.tsx +++ b/apps/roam/src/components/QueryEditor.tsx @@ -44,6 +44,8 @@ import { import getShallowTreeByParentUid from "roamjs-components/queries/getShallowTreeByParentUid"; import { ALL_SELECTION_SUGGESTIONS } from "~/utils/predefinedSelections"; import { getAlias } from "~/utils/parseResultSettings"; +import { setDiscourseNodeSetting } from "~/components/settings/utils/accessors"; +import { IndexSchema } from "~/components/settings/utils/zodSchema"; const getSourceCandidates = (cs: Condition[]): string[] => cs.flatMap((c) => @@ -434,6 +436,9 @@ type QueryEditorComponent = (props: { setHasResults?: () => void; hideCustomSwitch?: boolean; showAlias?: boolean; + discourseNodeType?: string; + settingKey?: "index" | "specification"; + returnNode?: string; }) => JSX.Element; const QueryEditor: QueryEditorComponent = ({ @@ -442,6 +447,9 @@ const QueryEditor: QueryEditorComponent = ({ setHasResults, hideCustomSwitch, showAlias, + discourseNodeType, // eslint-disable-line react/prop-types + settingKey, // eslint-disable-line react/prop-types + returnNode, // eslint-disable-line react/prop-types }) => { useEffect(() => { const previewQuery = ((e: CustomEvent) => { @@ -476,6 +484,53 @@ const QueryEditor: QueryEditorComponent = ({ const [conditions, _setConditions] = useState(initialConditions); const [selections, setSelections] = useState(initialSelections); const [custom, setCustom] = useState(initialCustom); + + const blockPropSyncTimeoutRef = useRef(0); + const lastSyncedIndexRef = useRef(""); + useEffect(() => { + return () => window.clearTimeout(blockPropSyncTimeoutRef.current); + }, []); + useEffect(() => { + if (!discourseNodeType || !settingKey) return; + + const stripped: unknown = JSON.parse( + JSON.stringify( + { conditions, selections, custom, returnNode }, + (key, value: unknown) => (key === "uid" ? undefined : value), + ), + ); + + const serialized = JSON.stringify(stripped); + if (serialized === lastSyncedIndexRef.current) return; + + const result = IndexSchema.safeParse(stripped); + if (!result.success) { + console.error( + `${settingKey} blockprop sync failed validation:`, + result.error, + ); + return; + } + + const path = + settingKey === "index" ? ["index"] : ["specification", "query"]; + + window.clearTimeout(blockPropSyncTimeoutRef.current); + blockPropSyncTimeoutRef.current = window.setTimeout(() => { + setDiscourseNodeSetting(discourseNodeType, path, result.data); + lastSyncedIndexRef.current = serialized; + }, 250); + + return () => window.clearTimeout(blockPropSyncTimeoutRef.current); + }, [ + conditions, + selections, + custom, + discourseNodeType, + settingKey, + returnNode, + ]); + const customNodeOnChange = (value: string) => { window.clearTimeout(debounceRef.current); setCustom(value); diff --git a/apps/roam/src/components/settings/AdminPanel.tsx b/apps/roam/src/components/settings/AdminPanel.tsx index 6606cf20e..85ba13269 100644 --- a/apps/roam/src/components/settings/AdminPanel.tsx +++ b/apps/roam/src/components/settings/AdminPanel.tsx @@ -38,6 +38,7 @@ import createBlock from "roamjs-components/writes/createBlock"; import deleteBlock from "roamjs-components/writes/deleteBlock"; import { USE_REIFIED_RELATIONS } from "~/data/userSettings"; import posthog from "posthog-js"; +import { setFeatureFlag } from "~/components/settings/utils/accessors"; const NodeRow = ({ node }: { node: PConceptFull }) => { return ( @@ -377,6 +378,7 @@ const FeatureFlagsTab = (): React.ReactElement => { setSuggestiveModeUid(undefined); } setSuggestiveModeEnabled(false); + setFeatureFlag("Suggestive mode enabled", false); } }} labelElement={ @@ -399,6 +401,7 @@ const FeatureFlagsTab = (): React.ReactElement => { }).then((uid) => { setSuggestiveModeUid(uid); setSuggestiveModeEnabled(true); + setFeatureFlag("Suggestive mode enabled", true); setIsAlertOpen(false); setIsInstructionOpen(true); }); @@ -447,6 +450,7 @@ const FeatureFlagsTab = (): React.ReactElement => { void setSetting(USE_REIFIED_RELATIONS, target.checked).catch( () => undefined, ); + setFeatureFlag("Reified relation triples", target.checked); posthog.capture("Reified Relations: Toggled", { enabled: target.checked, }); diff --git a/apps/roam/src/components/settings/DefaultFilters.tsx b/apps/roam/src/components/settings/DefaultFilters.tsx index f9daeb85f..693e98f74 100644 --- a/apps/roam/src/components/settings/DefaultFilters.tsx +++ b/apps/roam/src/components/settings/DefaultFilters.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react"; import type { OnloadArgs } from "roamjs-components/types"; import type { Filters } from "roamjs-components/components/Filter"; import posthog from "posthog-js"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; // // TODO - REWORK THIS COMPONENT @@ -124,28 +125,27 @@ const DefaultFilters = ({ ); useEffect(() => { - extensionAPI.settings.set( - "default-filters", - Object.fromEntries( - Object.entries(filters).map(([k, v]) => [ - k, - { - includes: Object.fromEntries( - Object.entries(v.includes || {}).map(([k, v]) => [ - k, - Array.from(v), - ]), - ), - excludes: Object.fromEntries( - Object.entries(v.excludes || {}).map(([k, v]) => [ - k, - Array.from(v), - ]), - ), - }, - ]), - ), + const serialized = Object.fromEntries( + Object.entries(filters).map(([k, v]) => [ + k, + { + includes: Object.fromEntries( + Object.entries(v.includes || {}).map(([k, v]) => [ + k, + Array.from(v), + ]), + ), + excludes: Object.fromEntries( + Object.entries(v.excludes || {}).map(([k, v]) => [ + k, + Array.from(v), + ]), + ), + }, + ]), ); + void extensionAPI.settings.set("default-filters", serialized); + setPersonalSetting(["Query", "Default filters"], serialized); }, [filters]); return (
void; onDelete: () => void }) => { + onSync, +}: Attribute & { + onChange: (v: string) => void; + onDelete: () => void; + onSync?: () => void; +}) => { const timeoutRef = useRef(0); return (
@@ -53,7 +60,16 @@ const NodeAttribute = ({ ); }; -const NodeAttributes = ({ uid }: { uid: string }) => { +const toRecord = (attrs: Attribute[]): Record => + Object.fromEntries(attrs.map((a) => [a.label, a.value])); + +const NodeAttributes = ({ + uid, + nodeType, +}: { + uid: string; + nodeType: string; +}) => { const [attributes, setAttributes] = useState(() => getBasicTreeByParentUid(uid).map((t) => ({ uid: t.uid, @@ -61,6 +77,15 @@ const NodeAttributes = ({ uid }: { uid: string }) => { value: t.children[0]?.text, })), ); + const attributesRef = useRef(attributes); + attributesRef.current = attributes; + const syncToBlockProps = () => { + setDiscourseNodeSetting( + nodeType, + ["attributes"], + toRecord(attributesRef.current), + ); + }; const [newAttribute, setNewAttribute] = useState(""); return (
@@ -77,10 +102,17 @@ const NodeAttributes = ({ uid }: { uid: string }) => { ) } onDelete={() => - deleteBlock(a.uid).then(() => - setAttributes(attributes.filter((aa) => a.uid !== aa.uid)), - ) + deleteBlock(a.uid).then(() => { + const updated = attributes.filter((aa) => a.uid !== aa.uid); + setAttributes(updated); + setDiscourseNodeSetting( + nodeType, + ["attributes"], + toRecord(updated), + ); + }) } + onSync={syncToBlockProps} /> ))}
@@ -105,11 +137,17 @@ const NodeAttributes = ({ uid }: { uid: string }) => { parentUid: uid, order: attributes.length, }).then((uid) => { - setAttributes([ + const updated = [ ...attributes, { uid, label: newAttribute, value: DEFAULT }, - ]); + ]; + setAttributes(updated); setNewAttribute(""); + setDiscourseNodeSetting( + nodeType, + ["attributes"], + toRecord(updated), + ); }); }} /> diff --git a/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx b/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx index ebc47d1ad..287f4194b 100644 --- a/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx @@ -11,7 +11,11 @@ import React, { useState, useMemo } from "react"; import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromTree"; import setInputSetting from "roamjs-components/util/setInputSetting"; -import { DiscourseNodeFlagPanel } from "./components/BlockPropSettingPanels"; +import { + DiscourseNodeFlagPanel, + DiscourseNodeTextPanel, +} from "./components/BlockPropSettingPanels"; +import { setDiscourseNodeSetting } from "~/components/settings/utils/accessors"; export const formatHexColor = (color: string) => { if (!color) return ""; @@ -37,9 +41,7 @@ const DiscourseNodeCanvasSettings = ({ const color = getSettingValueFromTree({ tree, key: "color" }); return formatHexColor(color); }); - const [alias, setAlias] = useState(() => - getSettingValueFromTree({ tree, key: "alias" }), - ); + const alias = getSettingValueFromTree({ tree, key: "alias" }); const [queryBuilderAlias, setQueryBuilderAlias] = useState(() => getSettingValueFromTree({ tree, key: "query-builder-alias" }), ); @@ -60,12 +62,18 @@ const DiscourseNodeCanvasSettings = ({ type={"color"} value={color} onChange={(e) => { + const colorValue = e.target.value.replace("#", ""); // remove hash to not create roam link setColor(e.target.value); void setInputSetting({ blockUid: uid, key: "color", - value: e.target.value.replace("#", ""), // remove hash to not create roam link + value: colorValue, }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "color"], + colorValue, + ); }} /> @@ -79,25 +87,30 @@ const DiscourseNodeCanvasSettings = ({ key: "color", value: "", }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "color"], + "", + ); }} />
- + { + void setInputSetting({ + blockUid: uid, + key: "alias", + value: val, + }); + }} + /> @@ -145,12 +163,18 @@ const DiscourseNodeCanvasSettings = ({ disabled={keyImageOption !== "query-builder" || !isKeyImage} value={queryBuilderAlias} onChange={(e) => { - setQueryBuilderAlias(e.target.value); + const val = e.target.value; + setQueryBuilderAlias(val); void setInputSetting({ blockUid: uid, key: "query-builder-alias", - value: e.target.value, + value: val, }); + setDiscourseNodeSetting( + nodeType, + ["canvasSettings", "query-builder-alias"], + val, + ); }} />
diff --git a/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx b/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx index ea5db06cc..8d388189e 100644 --- a/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx @@ -13,9 +13,14 @@ import refreshConfigTree from "~/utils/refreshConfigTree"; import createPage from "roamjs-components/writes/createPage"; import type { CustomField } from "roamjs-components/components/ConfigPanels/types"; import posthog from "posthog-js"; -import getDiscourseRelations from "~/utils/getDiscourseRelations"; +import getDiscourseRelations, { + type DiscourseRelation, +} from "~/utils/getDiscourseRelations"; import { deleteBlock } from "roamjs-components/writes"; import { formatHexColor } from "./DiscourseNodeCanvasSettings"; +import setBlockProps from "~/utils/setBlockProps"; +import { DiscourseNodeSchema } from "./utils/zodSchema"; +import { getGlobalSettings, setGlobalSetting } from "./utils/accessors"; type DiscourseNodeConfigPanelProps = React.ComponentProps< CustomField["options"]["component"] @@ -38,7 +43,9 @@ const DiscourseNodeConfigPanel: React.FC = ({ const [isAlertOpen, setIsAlertOpen] = useState(false); const [alertMessage, setAlertMessage] = useState(""); - const [affectedRelations, setAffectedRelations] = useState([]); + const [affectedRelations, setAffectedRelations] = useState< + DiscourseRelation[] + >([]); const [nodeTypeIdToDelete, setNodeTypeIdToDelete] = useState(""); const navigateToNode = (uid: string) => { if (isPopup) { @@ -72,13 +79,15 @@ const DiscourseNodeConfigPanel: React.FC = ({ className="select-none" disabled={!label} onClick={() => { + const shortcut = label.slice(0, 1).toUpperCase(); + const format = `[[${label.slice(0, 3).toUpperCase()}]] - {content}`; posthog.capture("Discourse Node: Type Created", { label: label }); - createPage({ + void createPage({ title: `discourse-graph/nodes/${label}`, tree: [ { text: "Shortcut", - children: [{ text: label.slice(0, 1).toUpperCase() }], + children: [{ text: shortcut }], }, { text: "Tag", @@ -86,14 +95,20 @@ const DiscourseNodeConfigPanel: React.FC = ({ }, { text: "Format", - children: [ - { - text: `[[${label.slice(0, 3).toUpperCase()}]] - {content}`, - }, - ], + children: [{ text: format }], }, ], }).then((valueUid) => { + setBlockProps( + valueUid, + DiscourseNodeSchema.parse({ + text: label, + type: valueUid, + shortcut, + format, + backedBy: "user", + }), + ); setNodes([ ...nodes, { @@ -222,6 +237,9 @@ const DiscourseNodeConfigPanel: React.FC = ({ throw error; }); } + const relations = { ...getGlobalSettings().Relations }; + for (const rel of affectedRelations) delete relations[rel.id]; + setGlobalSetting(["Relations"], relations); deleteNodeType(nodeTypeIdToDelete); } catch (error) { console.error( diff --git a/apps/roam/src/components/settings/DiscourseNodeIndex.tsx b/apps/roam/src/components/settings/DiscourseNodeIndex.tsx index 709a0fc00..38717ade2 100644 --- a/apps/roam/src/components/settings/DiscourseNodeIndex.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeIndex.tsx @@ -6,6 +6,7 @@ import type { DiscourseNode } from "~/utils/getDiscourseNodes"; import QueryBuilder from "~/components/QueryBuilder"; import parseQuery, { DEFAULT_RETURN_NODE } from "~/utils/parseQuery"; import createBlock from "roamjs-components/writes/createBlock"; +import { setDiscourseNodeSetting } from "~/components/settings/utils/accessors"; const NodeIndex = ({ parentUid, @@ -25,7 +26,7 @@ const NodeIndex = ({ ); useEffect(() => { if (!showQuery) { - createBlock({ + void createBlock({ parentUid: initialQueryArgs.conditionsNodesUid, node: { text: "clause", @@ -48,12 +49,37 @@ const NodeIndex = ({ }, ], }, - }).then(() => setShowQuery(true)); + }).then(() => { + setDiscourseNodeSetting(node.type, ["index"], { + conditions: [ + { + type: "clause", + source: DEFAULT_RETURN_NODE, + relation: "is a", + target: node.text, + }, + ], + selections: [], + custom: "", + returnNode: DEFAULT_RETURN_NODE, + }); + + setShowQuery(true); + }); } - }, [parentUid, initialQueryArgs, showQuery]); + }, [parentUid, initialQueryArgs, showQuery, node.text, node.type]); return ( - {showQuery ? : } + {showQuery ? ( + + ) : ( + + )} ); }; diff --git a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx index 5f2c7332f..b23404556 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx @@ -1,7 +1,6 @@ import React from "react"; import getSubTree from "roamjs-components/util/getSubTree"; import createBlock from "roamjs-components/writes/createBlock"; -import { Checkbox } from "@blueprintjs/core"; import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import deleteBlock from "roamjs-components/writes/deleteBlock"; import refreshConfigTree from "~/utils/refreshConfigTree"; @@ -9,6 +8,8 @@ import getDiscourseNodes from "~/utils/getDiscourseNodes"; import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression"; import QueryEditor from "~/components/QueryEditor"; import internalError from "~/utils/internalError"; +import { setDiscourseNodeSetting } from "~/components/settings/utils/accessors"; +import { DiscourseNodeFlagPanel } from "~/components/settings/components/BlockPropSettingPanels"; const NodeSpecification = ({ parentUid, @@ -20,11 +21,14 @@ const NodeSpecification = ({ parentSetEnabled?: (enabled: boolean) => void; }) => { const [migrated, setMigrated] = React.useState(false); - const [enabled, setEnabled] = React.useState( + const enabledBlockUid = React.useMemo( () => getSubTree({ tree: getBasicTreeByParentUid(parentUid), key: "enabled" }) ?.uid, + [parentUid], ); + const [enabled, setEnabled] = React.useState(!!enabledBlockUid); + React.useEffect(() => { if (enabled) { const scratchNode = getSubTree({ parentUid, key: "scratch" }); @@ -69,7 +73,22 @@ const NodeSpecification = ({ }, }), ) - .then(() => setMigrated(true)) + .then(() => { + setDiscourseNodeSetting(node.type, ["specification", "query"], { + conditions: [ + { + type: "clause" as const, + source: node.text, + relation: "has title", + target: `/${getDiscourseNodeFormatExpression(node.format).source}/`, + }, + ], + selections: [], + custom: "", + returnNode: node.text, + }); + setMigrated(true); + }) .catch((error) => { internalError({ error }); }); @@ -77,11 +96,18 @@ const NodeSpecification = ({ } else { const tree = getBasicTreeByParentUid(parentUid); const scratchNode = getSubTree({ tree, key: "scratch" }); - Promise.all(scratchNode.children.map((c) => deleteBlock(c.uid))).catch( - (error) => { + Promise.all(scratchNode.children.map((c) => deleteBlock(c.uid))) + .then(() => { + setDiscourseNodeSetting(node.type, ["specification", "query"], { + conditions: [], + selections: [], + custom: "", + returnNode: "", + }); + }) + .catch((error) => { internalError({ error }); - }, - ); + }); } return () => { refreshConfigTree(); @@ -90,40 +116,24 @@ const NodeSpecification = ({ return (
-

- { - const flag = (e.target as HTMLInputElement).checked; - if (flag) { - createBlock({ - parentUid, - order: 2, - node: { text: "enabled" }, - }) - .then((uid: string) => { - setEnabled(uid); - if (parentSetEnabled) parentSetEnabled(true); - }) - .catch((error) => { - internalError({ error }); - }); - } else { - deleteBlock(enabled) - .then(() => { - setEnabled(""); - if (parentSetEnabled) parentSetEnabled(false); - }) - .catch((error) => { - internalError({ error }); - }); - } - }} - /> -

+ { + setEnabled(checked); + parentSetEnabled?.(checked); + }} + />
@@ -131,6 +141,9 @@ const NodeSpecification = ({ parentUid={parentUid} key={Number(migrated)} hideCustomSwitch + discourseNodeType={node.type} + settingKey="specification" + returnNode={node.text} />
diff --git a/apps/roam/src/components/settings/DiscourseRelationConfigPanel.tsx b/apps/roam/src/components/settings/DiscourseRelationConfigPanel.tsx index e86681692..ce5638308 100644 --- a/apps/roam/src/components/settings/DiscourseRelationConfigPanel.tsx +++ b/apps/roam/src/components/settings/DiscourseRelationConfigPanel.tsx @@ -51,6 +51,10 @@ import { formatHexColor } from "./DiscourseNodeCanvasSettings"; import posthog from "posthog-js"; import { getSetting, setSetting } from "~/utils/extensionSettings"; import { USE_REIFIED_RELATIONS } from "~/data/userSettings"; +import { + setGlobalSetting, + getGlobalSettings, +} from "~/components/settings/utils/accessors"; const DEFAULT_SELECTED_RELATION = { display: "none", @@ -568,108 +572,147 @@ export const RelationEditPanel = ({ className="select-none" onClick={() => { setLoading(true); - setTimeout(async () => { - const rootUid = editingRelationInfo.uid; - setInputSetting({ - blockUid: rootUid, - key: "source", - value: source, - }); - setInputSetting({ - blockUid: rootUid, - key: "destination", - value: destination, - index: 1, - }); - setInputSetting({ - blockUid: rootUid, - key: "complement", - value: complement, - index: 2, - }); - updateBlock({ - uid: rootUid, - text: label, - }); - const ifUid = - editingRelationInfo.children.find((t) => - toFlexRegex("if").test(t.text), - )?.uid || - (await createBlock({ - node: { text: "If" }, - parentUid: rootUid, - order: 3, - })); - saveCyToElementRef(tab); - const blocks = tabs - .map((t) => elementsRef.current[t]) - .map((elements) => ({ - text: "And", - children: elements - .filter((e) => e.data.id.includes("-")) - .map((e) => { - const { source, target, relation } = e.data as { - source: string; - target: string; - relation: string; - }; - return { - text: ( - elements.find((e) => e.data.id === source)?.data as { - node: string; - } - )?.node, - children: [ - { - text: relation, + setTimeout( + () => + void (async () => { + const rootUid = editingRelationInfo.uid; + await setInputSetting({ + blockUid: rootUid, + key: "source", + value: source, + }); + await setInputSetting({ + blockUid: rootUid, + key: "destination", + value: destination, + index: 1, + }); + await setInputSetting({ + blockUid: rootUid, + key: "complement", + value: complement, + index: 2, + }); + await updateBlock({ + uid: rootUid, + text: label, + }); + const ifUid = + editingRelationInfo.children.find((t) => + toFlexRegex("if").test(t.text), + )?.uid || + (await createBlock({ + node: { text: "If" }, + parentUid: rootUid, + order: 3, + })); + saveCyToElementRef(tab); + const blocks = tabs + .map((t) => elementsRef.current[t]) + .map((elements) => ({ + text: "And", + children: elements + .filter((e) => e.data.id.includes("-")) + .map((e) => { + const { source, target, relation } = e.data as { + source: string; + target: string; + relation: string; + }; + return { + text: ( + elements.find((e) => e.data.id === source) + ?.data as { + node: string; + } + )?.node, children: [ { - text: ["source", "destination"].includes(target) - ? target - : ( - elements.find((e) => e.data.id === target) - ?.data as { node: string } - )?.node, + text: relation, + children: [ + { + text: ["source", "destination"].includes( + target, + ) + ? target + : ( + elements.find( + (e) => e.data.id === target, + )?.data as { node: string } + )?.node, + }, + ], }, ], + }; + }) + .concat([ + { + text: "node positions", + children: elements + .filter( + ( + e, + ): e is { + data: { id: string; node: unknown }; + position: { x: number; y: number }; + } => Object.keys(e).includes("position"), + ) + .map((e) => ({ + text: e.data.id, + children: [ + { text: `${e.position.x} ${e.position.y}` }, + ], + })), }, - ], - }; - }) - .concat([ - { - text: "node positions", - children: elements - .filter( - ( - e, - ): e is { - data: { id: string; node: unknown }; - position: { x: number; y: number }; - } => Object.keys(e).includes("position"), - ) - .map((e) => ({ - text: e.data.id, - children: [ - { text: `${e.position.x} ${e.position.y}` }, - ], - })), - }, - ]), - })); - await Promise.all( - getShallowTreeByParentUid(ifUid).map(({ uid }) => - deleteBlock(uid), - ), - ); - await Promise.all( - blocks.map((block, order) => - createBlock({ parentUid: ifUid, node: block, order }), - ), - ); - refreshConfigTree(); - back(); - }, 1); + ]), + })); + await Promise.all( + getShallowTreeByParentUid(ifUid).map(({ uid }) => + deleteBlock(uid), + ), + ); + await Promise.all( + blocks.map((block, order) => + createBlock({ parentUid: ifUid, node: block, order }), + ), + ); + refreshConfigTree(); + + const ifConditions = blocks.map((block) => { + const positionsChild = block.children.find( + (c) => c.text === "node positions", + ); + const triples = block.children + .filter((c) => c.text !== "node positions") + .map( + (c) => + [ + c.text, + c.children[0].text, + c.children[0].children[0].text, + ] as [string, string, string], + ); + const nodePositions = Object.fromEntries( + (positionsChild?.children ?? []).map((c) => [ + c.text, + c.children[0].text, + ]), + ); + return { triples, nodePositions }; + }); + setGlobalSetting(["Relations", rootUid], { + label, + source, + destination, + complement, + ifConditions, + }); + + back(); + })(), + 1, + ); }} /> @@ -1030,6 +1073,10 @@ const DiscourseRelationConfigPanel: CustomField["options"]["component"] = ({ const handleDelete = (rel: Relation) => { deleteBlock(rel.uid); setRelations(relations.filter((r) => r.uid !== rel.uid)); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/naming-convention + const { [rel.uid]: _, ...remaining } = getGlobalSettings().Relations; + setGlobalSetting(["Relations"], remaining); }; const handleDuplicate = (rel: Relation) => { const baseText = rel.text @@ -1059,7 +1106,15 @@ const DiscourseRelationConfigPanel: CustomField["options"]["component"] = ({ text, children: stripUid(copyTree), }, - }).then((newUid) => + }).then((newUid) => { + const originalRelation = getGlobalSettings().Relations[rel.uid]; + if (originalRelation) { + setGlobalSetting(["Relations", newUid], { + ...originalRelation, + label: text, + }); + } + setRelations([ ...relations, { @@ -1068,8 +1123,8 @@ const DiscourseRelationConfigPanel: CustomField["options"]["component"] = ({ destination: rel.destination, text, }, - ]), - ); + ]); + }); }; const handleBack = () => { setEditingRelation(""); diff --git a/apps/roam/src/components/settings/HomePersonalSettings.tsx b/apps/roam/src/components/settings/HomePersonalSettings.tsx index 01083caae..f4d2c74a3 100644 --- a/apps/roam/src/components/settings/HomePersonalSettings.tsx +++ b/apps/roam/src/components/settings/HomePersonalSettings.tsx @@ -58,6 +58,7 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { { - const platform = - typeof navigator !== "undefined" ? navigator.platform : undefined; - return platform == null ? false : /Mac|iPod|iPhone|iPad/.test(platform); -}; - -const MODIFIER_BIT_MASKS = { - alt: 1, - ctrl: 2, - meta: 4, - shift: 8, -}; - -const ALIASES: { [key: string]: string } = { - cmd: "meta", - command: "meta", - escape: "esc", - minus: "-", - mod: isMac() ? "meta" : "ctrl", - option: "alt", - plus: "+", - return: "enter", - win: "meta", -}; - -const normalizeKeyCombo = (combo: string) => { - const keys = combo.replace(/\s/g, "").split("+"); - return keys.map((key) => { - const keyName = ALIASES[key] != null ? ALIASES[key] : key; - return keyName === "meta" ? (isMac() ? "cmd" : "win") : keyName; - }); -}; - -const getModifiersFromCombo = (comboKey: IKeyCombo) => { - if (!comboKey) return []; - return [ - comboKey.modifiers & MODIFIER_BIT_MASKS.alt && "alt", - comboKey.modifiers & MODIFIER_BIT_MASKS.ctrl && "ctrl", - comboKey.modifiers & MODIFIER_BIT_MASKS.shift && "shift", - comboKey.modifiers & MODIFIER_BIT_MASKS.meta && "meta", - ].filter(Boolean); -}; - const KeyboardShortcutInput = ({ onloadArgs, settingKey, + blockPropKey, label, description, placeholder = "Click to set shortcut", @@ -104,6 +64,7 @@ const KeyboardShortcutInput = ({ extensionAPI.settings .set(settingKey, comboObj) .catch(() => console.error("Failed to set setting")); + setPersonalSetting([blockPropKey], comboObj); } return; } @@ -112,28 +73,26 @@ const KeyboardShortcutInput = ({ const comboObj = getKeyCombo(e.nativeEvent); if (!comboObj.key) return; - setComboKey({ key: comboObj.key, modifiers: comboObj.modifiers }); + const combo = { key: comboObj.key, modifiers: comboObj.modifiers }; + setComboKey(combo); extensionAPI.settings - .set(settingKey, comboObj) + .set(settingKey, combo) .catch(() => console.error("Failed to set setting")); + setPersonalSetting([blockPropKey], combo); }, - [extensionAPI, settingKey], + [extensionAPI, settingKey, blockPropKey], ); - const shortcut = useMemo(() => { - if (!comboKey.key) return ""; - - const modifiers = getModifiersFromCombo(comboKey); - const comboString = [...modifiers, comboKey.key].join("+"); - return normalizeKeyCombo(comboString).join("+"); - }, [comboKey]); + const shortcut = useMemo(() => comboToString(comboKey), [comboKey]); const handleClear = useCallback(() => { - setComboKey({ modifiers: 0, key: "" }); + const clearedCombo = { modifiers: 0, key: "" }; + setComboKey(clearedCombo); extensionAPI.settings - .set(settingKey, { modifiers: 0, key: "" }) + .set(settingKey, clearedCombo) .catch(() => console.error("Failed to set setting")); - }, [extensionAPI, settingKey]); + setPersonalSetting([blockPropKey], clearedCombo); + }, [extensionAPI, settingKey, blockPropKey]); return (