diff --git a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx index d6fa02e6b..fbdd1bc0f 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx @@ -1,78 +1,66 @@ 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 getDiscourseNodes from "~/utils/getDiscourseNodes"; import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression"; -import QueryEditor from "~/components/QueryEditor"; +import DiscourseNodeQueryEditor from "./components/DiscourseNodeQueryEditor"; +import { + getDiscourseNodeSetting, + setDiscourseNodeSetting, +} from "./utils/accessors"; +import type { Condition } from "~/utils/types"; + +const generateUID = (): string => + window.roamAlphaAPI?.util?.generateUID?.() ?? + Math.random().toString(36).substring(2, 11); const NodeSpecification = ({ - parentUid, node, }: { - parentUid: string; node: ReturnType[number]; }) => { - const [migrated, setMigrated] = React.useState(false); - const [enabled, setEnabled] = React.useState( - () => - getSubTree({ tree: getBasicTreeByParentUid(parentUid), key: "enabled" }) - ?.uid, - ); - React.useEffect(() => { - if (enabled) { - const scratchNode = getSubTree({ parentUid, key: "scratch" }); - if ( - !scratchNode.children.length || - !getSubTree({ tree: scratchNode.children, key: "conditions" }).children - .length - ) { - const conditionsUid = getSubTree({ - parentUid: scratchNode.uid, - key: "conditions", - }).uid; - const returnUid = getSubTree({ - parentUid: scratchNode.uid, - key: "return", - }).uid; - createBlock({ - parentUid: returnUid, - node: { - text: node.text, - }, - }) - .then(() => - createBlock({ - parentUid: conditionsUid, - node: { - text: "clause", - children: [ - { text: "source", children: [{ text: node.text }] }, - { text: "relation", children: [{ text: "has title" }] }, - { - text: "target", - children: [ - { - text: `/${ - getDiscourseNodeFormatExpression(node.format).source - }/`, - }, - ], - }, - ], - }, - }), - ) - .then(() => setMigrated(true)); + const nodeType = node.type; + + const [enabled, setEnabled] = React.useState(() => { + const spec = getDiscourseNodeSetting(nodeType, [ + "specification", + ]); + return spec !== null && spec !== undefined && spec.length > 0; + }); + + const createInitialCondition = React.useCallback((): Condition => { + return { + uid: generateUID(), + type: "clause", + source: node.text, + relation: "has title", + target: `/${getDiscourseNodeFormatExpression(node.format).source}/`, + }; + }, [node.text, node.format]); + + const handleEnabledChange = React.useCallback( + (e: React.FormEvent) => { + const flag = (e.target as HTMLInputElement).checked; + setEnabled(flag); + + if (flag) { + // Create initial condition when enabling + const existingSpec = getDiscourseNodeSetting(nodeType, [ + "specification", + ]); + if (!existingSpec || existingSpec.length === 0) { + const initialCondition = createInitialCondition(); + setDiscourseNodeSetting(nodeType, ["specification"], [ + initialCondition, + ]); + } + } else { + // Clear specification when disabling + setDiscourseNodeSetting(nodeType, ["specification"], []); } - } else { - const tree = getBasicTreeByParentUid(parentUid); - const scratchNode = getSubTree({ tree, key: "scratch" }); - Promise.all(scratchNode.children.map((c) => deleteBlock(c.uid))); - } - }, [parentUid, setMigrated, enabled]); + }, + [nodeType, createInitialCondition], + ); + return (

{ - const flag = (e.target as HTMLInputElement).checked; - if (flag) { - createBlock({ - parentUid, - order: 2, - node: { text: "enabled" }, - }).then(setEnabled); - } else { - deleteBlock(enabled).then(() => setEnabled("")); - } - }} + onChange={handleEnabledChange} />

-
diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index 85e4436b2..fd9728965 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -164,14 +164,13 @@ const NodeConfig = ({ onloadArgs: OnloadArgs; }) => { const suggestiveModeEnabled = useFeatureFlag("Suggestive Mode Enabled"); - // UIDs still needed for deferred complex settings (template, specification, etc.) + // UIDs still needed for deferred complex settings (template, index) const getUid = (key: string) => getSubTree({ parentUid: node.type, key: key, }).uid; const templateUid = getUid("Template"); - const specificationUid = getUid("Specification"); const indexUid = getUid("Index"); const [selectedTabId, setSelectedTabId] = useState("general"); @@ -329,10 +328,7 @@ const NodeConfig = ({ "The conditions specified to identify a ${nodeText} node." } /> - + } diff --git a/apps/roam/src/components/settings/components/DiscourseNodeQueryEditor.tsx b/apps/roam/src/components/settings/components/DiscourseNodeQueryEditor.tsx new file mode 100644 index 000000000..61e3a9723 --- /dev/null +++ b/apps/roam/src/components/settings/components/DiscourseNodeQueryEditor.tsx @@ -0,0 +1,591 @@ +import React, { useState, useCallback, useMemo, useRef } from "react"; +import { Button, H6, InputGroup, Tabs, Tab } from "@blueprintjs/core"; +import MenuItemSelect from "roamjs-components/components/MenuItemSelect"; +import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; +import { + getConditionLabels, + isTargetVariable, + sourceToTargetOptions, + sourceToTargetPlaceholder, +} from "~/utils/conditionToDatalog"; +import type { + Condition, + QBClause, + QBNot, + QBOr, + QBNor, + QBClauseData, + QBNestedData, +} from "~/utils/types"; +import { + getDiscourseNodeSetting, + setDiscourseNodeSetting, +} from "../utils/accessors"; + +const DEFAULT_RETURN_NODE = "node"; + +const generateUID = (): string => + window.roamAlphaAPI?.util?.generateUID?.() ?? + Math.random().toString(36).substring(2, 11); + +const getSourceCandidates = (cs: Condition[]): string[] => + cs.flatMap((c) => + c.type === "clause" || c.type === "not" + ? isTargetVariable({ relation: c.relation }) + ? [c.target] + : [] + : getSourceCandidates(c.conditions.flat()), + ); + +type QueryClauseProps = { + con: QBClause | QBNot; + index: number; + setConditions: React.Dispatch>; + getAvailableVariables: (index: number) => string[]; + onSave: () => void; +}; + +const QueryClause = ({ + con, + index, + setConditions, + getAvailableVariables, + onSave, +}: QueryClauseProps) => { + const debounceRef = useRef(0); + const conditionLabels = useMemo(getConditionLabels, []); + const targetOptions = useMemo( + () => sourceToTargetOptions({ source: con.source, relation: con.relation }), + [con.source, con.relation], + ); + const targetPlaceholder = useMemo( + () => sourceToTargetPlaceholder({ relation: con.relation }), + [con.relation], + ); + + const setConditionRelation = useCallback( + (e: string, timeout: boolean = true) => { + window.clearTimeout(debounceRef.current); + setConditions((conditions) => + conditions.map((c) => (c.uid === con.uid ? { ...c, relation: e } : c)), + ); + debounceRef.current = window.setTimeout( + () => onSave(), + timeout ? 1000 : 0, + ); + }, + [setConditions, con.uid, onSave], + ); + + const setConditionTarget = useCallback( + (e: string, timeout: boolean = true) => { + window.clearTimeout(debounceRef.current); + setConditions((conditions) => + conditions.map((c) => (c.uid === con.uid ? { ...c, target: e } : c)), + ); + debounceRef.current = window.setTimeout( + () => onSave(), + timeout ? 1000 : 0, + ); + }, + [setConditions, con.uid, onSave], + ); + + const availableSources = useMemo( + () => getAvailableVariables(index), + [getAvailableVariables, index], + ); + + return ( + <> + { + setConditions((conditions) => + conditions.map((c) => + c.uid === con.uid ? { ...con, source: value } : c, + ), + ); + onSave(); + }} + /> +
+ setConditionRelation(e, false)} + options={conditionLabels} + placeholder={"Choose relationship"} + id={`${con.uid}-relation`} + /> +
+
+ setConditionTarget(e, false)} + options={targetOptions} + placeholder={targetPlaceholder} + id={`${con.uid}-target`} + /> +
+ + ); +}; + +type QueryNestedDataProps = { + con: QBOr | QBNor; + setView: (s: { uid: string; branch: number }) => void; +}; + +const QueryNestedData = ({ con, setView }: QueryNestedDataProps) => { + return ( + <> + +