>;
+ getAvailableVariables: (n: number) => string[];
+ setView: (s: { uid: string; branch: number }) => void;
+ onSave: () => void;
+ onDelete: (uid: string) => void;
+};
+
+const QueryCondition = ({
+ con,
+ index,
+ setConditions,
+ getAvailableVariables,
+ setView,
+ onSave,
+ onDelete,
+}: QueryConditionProps) => {
+ return (
+
+ {
+ const isChangingStructure =
+ ((con.type === "or" || con.type === "not or") &&
+ (value === "clause" || value === "not")) ||
+ ((value === "or" || value === "not or") &&
+ (con.type === "clause" || con.type === "not"));
+
+ setConditions((conditions) =>
+ conditions.map((c) => {
+ if (c.uid !== con.uid) return c;
+
+ if (value === "clause" || value === "not") {
+ return {
+ uid: c.uid,
+ type: value,
+ source: (c as QBClauseData).source || DEFAULT_RETURN_NODE,
+ target: (c as QBClauseData).target || "",
+ relation: (c as QBClauseData).relation || "",
+ };
+ } else {
+ return {
+ uid: c.uid,
+ type: value,
+ conditions: isChangingStructure
+ ? []
+ : (c as QBNestedData).conditions || [],
+ };
+ }
+ }),
+ );
+ onSave();
+ }}
+ />
+ {(con.type === "clause" || con.type === "not") && (
+
+ )}
+ {(con.type === "not or" || con.type === "or") && (
+
+ )}
+
+ );
+};
+
+const getConditionByUid = (
+ uid: string,
+ conditions: Condition[],
+): Condition | undefined => {
+ for (const con of conditions) {
+ if (con.uid === uid) return con;
+ if (con.type === "or" || con.type === "not or") {
+ const c = getConditionByUid(uid, con.conditions.flat());
+ if (c) return c;
+ }
+ }
+ return undefined;
+};
+
+type DiscourseNodeQueryEditorProps = {
+ nodeType: string;
+ defaultConditions?: Condition[];
+ settingKeys?: string[];
+};
+
+const DiscourseNodeQueryEditor = ({
+ nodeType,
+ defaultConditions = [],
+ settingKeys = ["specification"],
+}: DiscourseNodeQueryEditorProps) => {
+ const [conditions, _setConditions] = useState(() => {
+ const stored = getDiscourseNodeSetting(nodeType, settingKeys);
+ return stored && stored.length > 0 ? stored : defaultConditions;
+ });
+
+ const saveConditions = useCallback(
+ (newConditions: Condition[]) => {
+ setDiscourseNodeSetting(nodeType, settingKeys, newConditions);
+ },
+ [nodeType, settingKeys],
+ );
+
+ const setConditions: React.Dispatch> =
+ useCallback(
+ (action) => {
+ _setConditions((prev) => {
+ const next = typeof action === "function" ? action(prev) : action;
+ return next;
+ });
+ },
+ [_setConditions],
+ );
+
+ const handleSave = useCallback(() => {
+ _setConditions((current) => {
+ saveConditions(current);
+ return current;
+ });
+ }, [saveConditions]);
+
+ const [viewStack, setViewStack] = useState([{ uid: nodeType, branch: 0 }]);
+ const view = useMemo(() => viewStack.slice(-1)[0], [viewStack]);
+ const viewCondition = useMemo(
+ () =>
+ view.uid === nodeType
+ ? undefined
+ : (getConditionByUid(view.uid, conditions) as QBOr | QBNor | undefined),
+ [view, conditions, nodeType],
+ );
+
+ const nestedSetConditions = useMemo<
+ React.Dispatch>
+ >(() => {
+ if (view.uid === nodeType) return setConditions;
+ return (nestedConditions) => {
+ if (!viewCondition) return;
+ setConditions((cons) => {
+ const updateNested = (conditions: Condition[]): Condition[] =>
+ conditions.map((c): Condition => {
+ if (c.uid === viewCondition.uid && (c.type === "or" || c.type === "not or")) {
+ const newConditions = [...c.conditions];
+ if (typeof nestedConditions === "function") {
+ newConditions[view.branch] = nestedConditions(
+ newConditions[view.branch] || [],
+ );
+ } else {
+ newConditions[view.branch] = nestedConditions;
+ }
+ return { ...c, conditions: newConditions };
+ }
+ if (c.type === "or" || c.type === "not or") {
+ return {
+ ...c,
+ conditions: c.conditions.map((branch) => updateNested(branch)),
+ };
+ }
+ return c;
+ });
+ return updateNested(cons);
+ });
+ };
+ }, [setConditions, view.uid, nodeType, viewCondition, view.branch]);
+
+ const viewConditions = useMemo(
+ () =>
+ view.uid === nodeType
+ ? conditions
+ : viewCondition?.conditions?.[view.branch] || [],
+ [view, viewCondition, conditions, nodeType],
+ );
+
+ const getAvailableVariables = useCallback(
+ (index: number) =>
+ Array.from(
+ new Set(getSourceCandidates(viewConditions.slice(0, index))),
+ ).concat(DEFAULT_RETURN_NODE),
+ [viewConditions],
+ );
+
+ const addCondition = useCallback(() => {
+ const newCondition: QBClause = {
+ uid: generateUID(),
+ source: DEFAULT_RETURN_NODE,
+ relation: "",
+ target: "",
+ type: "clause",
+ };
+ nestedSetConditions((cons) => [...cons, newCondition]);
+ setTimeout(() => {
+ handleSave();
+ document.getElementById(`${newCondition.uid}-relation`)?.focus();
+ }, 0);
+ }, [nestedSetConditions, handleSave]);
+
+ const deleteCondition = useCallback(
+ (uid: string) => {
+ nestedSetConditions((cons) => cons.filter((c) => c.uid !== uid));
+ setTimeout(() => handleSave(), 0);
+ },
+ [nestedSetConditions, handleSave],
+ );
+
+ const createBranch = useCallback(() => {
+ if (!viewCondition) return;
+ const newBranch = viewCondition.conditions.length;
+ setConditions((cons) => {
+ const updateNested = (conditions: Condition[]): Condition[] =>
+ conditions.map((c): Condition => {
+ if (c.uid === viewCondition.uid && (c.type === "or" || c.type === "not or")) {
+ return { ...c, conditions: [...c.conditions, []] };
+ }
+ if (c.type === "or" || c.type === "not or") {
+ return {
+ ...c,
+ conditions: c.conditions.map((branch) => updateNested(branch)),
+ };
+ }
+ return c;
+ });
+ return updateNested(cons);
+ });
+ setViewStack((vs) =>
+ vs.slice(0, -1).concat({ uid: view.uid, branch: newBranch }),
+ );
+ setTimeout(() => handleSave(), 0);
+ }, [view.uid, viewCondition, setConditions, handleSave]);
+
+ const deleteBranch = useCallback(() => {
+ if (!viewCondition || viewCondition.conditions.length <= 1) return;
+ setConditions((cons) => {
+ const updateNested = (conditions: Condition[]): Condition[] =>
+ conditions.map((c): Condition => {
+ if (c.uid === viewCondition.uid && (c.type === "or" || c.type === "not or")) {
+ return {
+ ...c,
+ conditions: c.conditions.filter((_, i) => i !== view.branch),
+ };
+ }
+ if (c.type === "or" || c.type === "not or") {
+ return {
+ ...c,
+ conditions: c.conditions.map((branch) => updateNested(branch)),
+ };
+ }
+ return c;
+ });
+ return updateNested(cons);
+ });
+ setViewStack((vs) =>
+ vs.slice(0, -1).concat({
+ uid: view.uid,
+ branch: view.branch === 0 ? 0 : view.branch - 1,
+ }),
+ );
+ setTimeout(() => handleSave(), 0);
+ }, [view, viewCondition, setConditions, handleSave]);
+
+ // Main view
+ if (view.uid === nodeType) {
+ return (
+
+
+ FIND
+
+
+ WHERE
+
+
+ {conditions.map((con, index) => (
+
setViewStack([...viewStack, v])}
+ onSave={handleSave}
+ onDelete={deleteCondition}
+ />
+ ))}
+
+
+
+
+
+
+ );
+ }
+
+ // Nested OR/NOT OR view
+ return (
+
+
+
OR Branches
+
+ setViewStack(
+ viewStack
+ .slice(0, -1)
+ .concat([{ uid: view.uid, branch: Number(e) }]),
+ )
+ }
+ >
+ {viewCondition &&
+ Array(viewCondition.conditions.length)
+ .fill(null)
+ .map((_, j) => (
+
+ {viewConditions.map((con, index) => (
+ setViewStack([...viewStack, v])}
+ onSave={handleSave}
+ onDelete={deleteCondition}
+ />
+ ))}
+ >
+ }
+ />
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DiscourseNodeQueryEditor;
diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts
index 8d9fa2516..1bafa761d 100644
--- a/apps/roam/src/components/settings/utils/zodSchema.ts
+++ b/apps/roam/src/components/settings/utils/zodSchema.ts
@@ -120,7 +120,13 @@ export const DiscourseNodeSchema = z.object({
.optional()
.transform((val) => val ?? {}),
overlay: stringWithDefault(""),
- index: z.unknown().nullable().optional(),
+ index: z
+ .object({
+ conditions: z.array(ConditionSchema).default([]),
+ selections: z.array(SelectionSchema).default([]),
+ })
+ .nullable()
+ .optional(),
suggestiveRules: SuggestiveRulesSchema.nullable().optional(),
embeddingRef: stringWithDefault(""),
isFirstChild: z
diff --git a/apps/roam/src/utils/renderNodeConfigPage.ts b/apps/roam/src/utils/renderNodeConfigPage.ts
index 7542397ec..e3880f646 100644
--- a/apps/roam/src/utils/renderNodeConfigPage.ts
+++ b/apps/roam/src/utils/renderNodeConfigPage.ts
@@ -49,11 +49,9 @@ export const renderNodeConfigPage = ({
description: "Index of all of the pages in your graph of this type",
Panel: CustomPanel,
options: {
- component: ({ uid }) =>
+ component: () =>
React.createElement(DiscourseNodeIndex, {
node,
- parentUid: uid,
- onloadArgs,
}),
},
} as Field,
@@ -73,10 +71,9 @@ export const renderNodeConfigPage = ({
description: `The conditions specified to identify a ${nodeText} node.`,
Panel: CustomPanel,
options: {
- component: ({ uid }) =>
+ component: () =>
React.createElement(DiscourseNodeSpecification, {
node,
- parentUid: uid,
}),
},
} as Field,