diff --git a/apps/roam/src/components/QueryBuilder.tsx b/apps/roam/src/components/QueryBuilder.tsx index 20b5cbf47..ac7fd1897 100644 --- a/apps/roam/src/components/QueryBuilder.tsx +++ b/apps/roam/src/components/QueryBuilder.tsx @@ -32,6 +32,8 @@ type QueryPageComponent = (props: { isEditBlock?: boolean; showAlias?: boolean; discourseNodeType?: string; + settingKey?: "index" | "specification"; + returnNode?: string; }) => JSX.Element; type Props = Parameters[0]; @@ -41,6 +43,8 @@ const QueryBuilder = ({ isEditBlock, showAlias, discourseNodeType, + settingKey, + returnNode, }: Props) => { const extensionAPI = useExtensionAPI(); const hideMetadata = useMemo( @@ -165,6 +169,8 @@ const QueryBuilder = ({ { setHasResults(true); setIsEdit(false); diff --git a/apps/roam/src/components/QueryEditor.tsx b/apps/roam/src/components/QueryEditor.tsx index 4b9a6ee61..1be46ae0a 100644 --- a/apps/roam/src/components/QueryEditor.tsx +++ b/apps/roam/src/components/QueryEditor.tsx @@ -437,6 +437,8 @@ type QueryEditorComponent = (props: { hideCustomSwitch?: boolean; showAlias?: boolean; discourseNodeType?: string; + settingKey?: "index" | "specification"; + returnNode?: string; }) => JSX.Element; const QueryEditor: QueryEditorComponent = ({ @@ -446,6 +448,8 @@ const QueryEditor: QueryEditorComponent = ({ hideCustomSwitch, showAlias, discourseNodeType, + settingKey, // eslint-disable-line react/prop-types + returnNode, // eslint-disable-line react/prop-types }) => { useEffect(() => { const previewQuery = ((e: CustomEvent) => { @@ -487,11 +491,11 @@ const QueryEditor: QueryEditorComponent = ({ return () => window.clearTimeout(blockPropSyncTimeoutRef.current); }, []); useEffect(() => { - if (!discourseNodeType) return; + if (!discourseNodeType || !settingKey) return; const stripped: unknown = JSON.parse( JSON.stringify( - { conditions, selections, custom }, + { conditions, selections, custom, returnNode }, (key, value: unknown) => (key === "uid" ? undefined : value), ), ); @@ -501,18 +505,31 @@ const QueryEditor: QueryEditorComponent = ({ const result = IndexSchema.safeParse(stripped); if (!result.success) { - console.error("Index blockprop sync failed validation:", result.error); + 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, ["index"], result.data); + setDiscourseNodeSetting(discourseNodeType, path, result.data); lastSyncedIndexRef.current = serialized; }, 250); return () => window.clearTimeout(blockPropSyncTimeoutRef.current); - }, [conditions, selections, custom, discourseNodeType]); + }, [ + conditions, + selections, + custom, + discourseNodeType, + settingKey, + returnNode, + ]); const customNodeOnChange = (value: string) => { window.clearTimeout(debounceRef.current); diff --git a/apps/roam/src/components/settings/DiscourseNodeIndex.tsx b/apps/roam/src/components/settings/DiscourseNodeIndex.tsx index ad1b0a40e..38717ade2 100644 --- a/apps/roam/src/components/settings/DiscourseNodeIndex.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeIndex.tsx @@ -60,6 +60,8 @@ const NodeIndex = ({ }, ], selections: [], + custom: "", + returnNode: DEFAULT_RETURN_NODE, }); setShowQuery(true); @@ -69,7 +71,12 @@ const NodeIndex = ({ return ( {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/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts index f862c3f83..6dba8505f 100644 --- a/apps/roam/src/components/settings/utils/init.ts +++ b/apps/roam/src/components/settings/utils/init.ts @@ -74,7 +74,13 @@ const initializeSettingsBlockProps = ( const uid = blockMap[key]; if (uid) { const existingProps = getBlockProps(uid); - if (!existingProps || Object.keys(existingProps).length === 0) { + // TODO: Overwriting on safeParse failure is a temporary fix for schema shape changes + // (e.g. specification: [] -> {enabled, query}). Replace with proper versioned migrations. + if ( + !existingProps || + Object.keys(existingProps).length === 0 || + !schema.safeParse(existingProps).success + ) { const defaults = schema.parse({}); setBlockProps(uid, defaults, false); } @@ -108,7 +114,12 @@ const initSingleDiscourseNode = async ( ); const existingProps = getBlockProps(pageUid); - if (!existingProps || Object.keys(existingProps).length === 0) { + // TODO: Same temporary fix as initializeSettingsBlockProps — replace with proper migrations. + if ( + !existingProps || + Object.keys(existingProps).length === 0 || + !DiscourseNodeSchema.safeParse(existingProps).success + ) { const nodeData = DiscourseNodeSchema.parse({ text: node.text, type: node.type, diff --git a/apps/roam/src/components/settings/utils/zodSchema.example.ts b/apps/roam/src/components/settings/utils/zodSchema.example.ts index d72b6b7c0..b8c2d04f6 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.example.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.example.ts @@ -45,14 +45,22 @@ const discourseNodeSettings: DiscourseNodeSettings = { shortcut: "C", tag: "#claim", description: "A statement or assertion that can be supported or refuted", - specification: [ - { - type: "clause", - source: "Claim", - relation: "has title", - target: "/^\\[\\[CLM\\]\\]/", + specification: { + enabled: true, + query: { + conditions: [ + { + type: "clause", + source: "Claim", + relation: "has title", + target: "/^\\[\\[CLM\\]\\]/", + }, + ], + selections: [], + custom: "", + returnNode: "Claim", }, - ], + }, template: [ { text: "Summary::", heading: 2 }, { text: "Evidence::", heading: 2, children: [{ text: "" }] }, @@ -76,6 +84,7 @@ const discourseNodeSettings: DiscourseNodeSettings = { ], selections: [], custom: "", + returnNode: "node", }, suggestiveRules, backedBy: "user", diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts index 5e49a1bc3..6d746c621 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -53,6 +53,7 @@ export const IndexSchema = z.object({ conditions: z.array(ConditionSchema).default([]), selections: z.array(SelectionSchema).default([]), custom: z.string().default(""), + returnNode: z.string().default("node"), }); type RoamNode = { @@ -103,10 +104,19 @@ export const DiscourseNodeSchema = z.object({ tag: stringWithDefault(""), description: stringWithDefault(""), specification: z - .array(ConditionSchema) + .object({ + enabled: z.boolean().default(false), + query: IndexSchema.default({}), + }) .nullable() .optional() - .transform((val) => val ?? []), + .transform( + (val) => + val ?? { + enabled: false, + query: { conditions: [], selections: [], custom: "", returnNode: "" }, + }, + ), template: z .array(RoamNodeSchema) .nullable()