Skip to content
Merged
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
142 changes: 142 additions & 0 deletions apps/roam/src/components/settings/data/defaultRelationsBlockProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import type { DiscourseRelationSettings } from "~/components/settings/utils/zodSchema";
/* eslint-disable @typescript-eslint/naming-convention */ // This is for nodePosition keys

// TODO: Delete the original default relations in data/defaultRelations.ts when fully migrated.
const DEFAULT_RELATIONS_BLOCK_PROPS: DiscourseRelationSettings[] = [
{
id: "informs",
label: "Informs",
source: "_EVD-node",
destination: "_QUE-node",
complement: "Informed By",
ifConditions: [
{
triples: [
["Page", "is a", "source"],
["Block", "references", "Page"],
["Block", "is in page", "ParentPage"],
["ParentPage", "is a", "destination"],
],
nodePositions: {
"0": "100 57",
"1": "100 208",
"2": "100 345",
source: "281 57",
destination: "281 345",
},
},
],
},
{
id: "supports",
label: "Supports",
source: "_EVD-node",
destination: "_CLM-node",
complement: "Supported By",
ifConditions: [
{
triples: [
["Page", "is a", "source"],
["Block", "references", "Page"],
["SBlock", "references", "SPage"],
["SPage", "has title", "SupportedBy"],
["SBlock", "has child", "Block"],
["PBlock", "references", "ParentPage"],
["PBlock", "has child", "SBlock"],
["ParentPage", "is a", "destination"],
],
nodePositions: {
"0": "250 325",
"1": "100 325",
"2": "100 200",
"3": "250 200",
"4": "400 200",
"5": "100 75",
"6": "250 75",
source: "400 325",
destination: "400 75",
},
},
{
triples: [
["Page", "is a", "destination"],
["Block", "references", "Page"],
["SBlock", "references", "SPage"],
["SPage", "has title", "Supports"],
["SBlock", "has child", "Block"],
["PBlock", "references", "ParentPage"],
["PBlock", "has child", "SBlock"],
["ParentPage", "is a", "source"],
],
nodePositions: {
"7": "250 325",
"8": "100 325",
"9": "100 200",
"10": "250 200",
"11": "400 200",
"12": "100 75",
"13": "250 75",
source: "400 75",
destination: "400 325",
},
},
],
},
{
id: "opposes",
label: "Opposes",
source: "_EVD-node",
destination: "_CLM-node",
complement: "Opposed By",
ifConditions: [
{
triples: [
["Page", "is a", "source"],
["Block", "references", "Page"],
["SBlock", "references", "SPage"],
["SPage", "has title", "OpposedBy"],
["SBlock", "has child", "Block"],
["PBlock", "references", "ParentPage"],
["PBlock", "has child", "SBlock"],
["ParentPage", "is a", "destination"],
],
nodePositions: {
"0": "250 325",
"1": "100 325",
"2": "100 200",
"3": "250 200",
"4": "400 200",
"5": "100 75",
"6": "250 75",
source: "400 325",
destination: "400 75",
},
},
{
triples: [
["Page", "is a", "destination"],
["Block", "references", "Page"],
["SBlock", "references", "SPage"],
["SPage", "has title", "Opposes"],
["SBlock", "has child", "Block"],
["PBlock", "references", "ParentPage"],
["PBlock", "has child", "SBlock"],
["ParentPage", "is a", "source"],
],
nodePositions: {
"7": "250 325",
"8": "100 325",
"9": "100 200",
"10": "250 200",
"11": "400 200",
"12": "100 75",
"13": "250 75",
source: "400 75",
destination: "400 325",
},
},
],
},
];

export default DEFAULT_RELATIONS_BLOCK_PROPS;
224 changes: 224 additions & 0 deletions apps/roam/src/components/settings/utils/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
import getShallowTreeByParentUid from "roamjs-components/queries/getShallowTreeByParentUid";
import { createPage, createBlock } from "roamjs-components/writes";
import setBlockProps from "~/utils/setBlockProps";
import getBlockProps from "~/utils/getBlockProps";
import INITIAL_NODE_VALUES from "~/data/defaultDiscourseNodes";
import {
DiscourseNodeSchema,
getTopLevelBlockPropsConfig,
getPersonalSettingsKey,
} from "~/components/settings/utils/zodSchema";
import { DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, DISCOURSE_NODE_PAGE_PREFIX } from "./zodSchema";

const ensurePageExists = async (pageTitle: string): Promise<string> => {
let pageUid = getPageUidByPageTitle(pageTitle);

if (!pageUid) {
pageUid = window.roamAlphaAPI.util.generateUID();
await createPage({
title: pageTitle,
uid: pageUid,
});
}

return pageUid;
};

const ensureBlocksExist = async (
pageUid: string,
blockTexts: string[],
existingBlockMap: Record<string, string>,
): Promise<Record<string, string>> => {
const missingBlocks = blockTexts.filter(
(blockText) => !existingBlockMap[blockText],
);

if (missingBlocks.length > 0) {
const createdBlocks = await Promise.all(
missingBlocks.map(async (blockText) => {
const uid = await createBlock({
parentUid: pageUid,
node: { text: blockText },
});
return { text: blockText, uid };
}),
);

createdBlocks.forEach((block) => {
existingBlockMap[block.text] = block.uid;
});
}

return existingBlockMap;
};

const buildBlockMap = (pageUid: string): Record<string, string> => {
const existingChildren = getShallowTreeByParentUid(pageUid);
const blockMap: Record<string, string> = {};
existingChildren.forEach((child) => {
blockMap[child.text] = child.uid;
});
return blockMap;
};

const initializeSettingsBlockProps = (
blockMap: Record<string, string>,
): void => {
const configs = getTopLevelBlockPropsConfig();

for (const { key, schema } of configs) {
const uid = blockMap[key];
if (uid) {
const existingProps = getBlockProps(uid);
if (!existingProps || Object.keys(existingProps).length === 0) {
const defaults = schema.parse({});
setBlockProps(uid, defaults, false);
}
}
}
};

const initSettingsPageBlocks = async (): Promise<Record<string, string>> => {
const pageUid = await ensurePageExists(DG_BLOCK_PROP_SETTINGS_PAGE_TITLE);
const blockMap = buildBlockMap(pageUid);

const topLevelBlocks = getTopLevelBlockPropsConfig().map(({ key }) => key);
await ensureBlocksExist(pageUid, topLevelBlocks, blockMap);

initializeSettingsBlockProps(blockMap);

return blockMap;
};

const hasNonDefaultNodes = (): boolean => {
const results = window.roamAlphaAPI.q(`
Copy link
Contributor

@mdroidian mdroidian Jan 23, 2026

Choose a reason for hiding this comment

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

Let's use the existing pattern:

  const nodes = getDiscourseNodes().filter(excludeDefaultNodes);
  if (nodes.length === 0) {

It's a single line and if we ever change how discourse nodes are defined, it won't break hasNonDefaultNodes().

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

getDiscourseNodes uses the discourseConfigRef.tree which underneath uses the roam block data and we are migrating from that, therefore we have this

Copy link
Contributor

Choose a reason for hiding this comment

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

@sid597 We'll need to recreate a proper getDiscourseNodes() then.

Copy link
Collaborator Author

@sid597 sid597 Jan 24, 2026

Choose a reason for hiding this comment

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

I have this same function as the getDiscourseNodes() in the accessors pr, by proper you mean smth similar to the existing one or only in performance? I think we should discuss this in the accessors pr #669

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean getDiscourseNodes() is used in many places across the repo. When we move from blocks to props, we'll need a getDiscourseNodes() that uses the props.
Regardless of where the new getDiscourseNodes() is created, it should be used in hasNonDefaultNodes() to make sure we DRY.

As an aside, hasNonDefaultNodes (or the new getDiscourseNodes()) should use fast.q and return the props in the single query, not an additional pull for each result

[:find ?uid ?title
:where
[?page :node/title ?title]
[?page :block/uid ?uid]
[(clojure.string/starts-with? ?title "${DISCOURSE_NODE_PAGE_PREFIX}")]]
`) as [string, string][];

for (const [pageUid] of results) {
const blockProps = getBlockProps(pageUid);
if (!blockProps) continue;

const parsed = DiscourseNodeSchema.safeParse(blockProps);
if (!parsed.success) continue;

if (parsed.data.backedBy !== "default") {
return true;
}
}

return false;
};

const initSingleDiscourseNode = async (
node: (typeof INITIAL_NODE_VALUES)[number],
): Promise<{ label: string; pageUid: string } | null> => {
if (!node.text) return null;

const pageUid = await ensurePageExists(
`${DISCOURSE_NODE_PAGE_PREFIX}${node.text}`,
);
const existingProps = getBlockProps(pageUid);

if (!existingProps || Object.keys(existingProps).length === 0) {
const nodeData = DiscourseNodeSchema.parse({
text: node.text,
type: node.type,
format: node.format || "",
shortcut: node.shortcut || "",
tag: node.tag || "",
graphOverview: node.graphOverview ?? false,
canvasSettings: node.canvasSettings || {},
backedBy: "default",
});

setBlockProps(pageUid, nodeData, false);
}

return { label: node.text, pageUid };
};

const initDiscourseNodePages = async (): Promise<Record<string, string>> => {
if (hasNonDefaultNodes()) {
return {};
}

const results = await Promise.all(
INITIAL_NODE_VALUES.map((node) => initSingleDiscourseNode(node)),
);

const nodePageUids: Record<string, string> = {};
for (const result of results) {
if (result) {
nodePageUids[result.label] = result.pageUid;
}
}

return nodePageUids;
};

const printAllSettings = (
blockMap: Record<string, string>,
nodePageUids: Record<string, string>,
): void => {
const configs = getTopLevelBlockPropsConfig();
const featureFlagsUid = blockMap[configs.find(({ key }) => key === "Feature Flags")?.key ?? ""];
const globalUid = blockMap[configs.find(({ key }) => key === "Global")?.key ?? ""];
const personalKey = getPersonalSettingsKey();
const personalUid = blockMap[personalKey];

const featureFlags = featureFlagsUid ? getBlockProps(featureFlagsUid) : null;
const globalSettings = globalUid ? getBlockProps(globalUid) : null;
const personalSettings = personalUid ? getBlockProps(personalUid) : null;

console.group("🔧 Discourse Graph Settings Initialized (RAW DATA)");

console.group(`🚩 Feature Flags (uid: ${featureFlagsUid})`);
console.log("Raw block props:", JSON.stringify(featureFlags, null, 2));
console.groupEnd();

console.group(`🌍 Global Settings (uid: ${globalUid})`);
console.log("Raw block props:", JSON.stringify(globalSettings, null, 2));
console.groupEnd();

console.group(`👤 Personal Settings (uid: ${personalUid})`);
console.log("Raw block props:", JSON.stringify(personalSettings, null, 2));
console.groupEnd();

console.group("📝 Discourse Nodes");
for (const [nodeLabel, pageUid] of Object.entries(nodePageUids)) {
const nodeProps = getBlockProps(pageUid);
console.group(`${nodeLabel} (uid: ${pageUid})`);
console.log("Raw block props:", JSON.stringify(nodeProps, null, 2));
console.groupEnd();
}
console.groupEnd();

const relations = (globalSettings as Record<string, unknown>)?.Relations;
console.group("🔗 Discourse Relations");
console.log("Relations:", JSON.stringify(relations, null, 2));
console.groupEnd();

console.groupEnd();
};

export type InitSchemaResult = {
blockUids: Record<string, string>;
nodePageUids: Record<string, string>;
};

export const initSchema = async (): Promise<InitSchemaResult> => {
const blockUids = await initSettingsPageBlocks();
const nodePageUids = await initDiscourseNodePages();

setTimeout(() => {
printAllSettings(blockUids, nodePageUids);
}, 2000);

return { blockUids, nodePageUids };
};
18 changes: 11 additions & 7 deletions apps/roam/src/components/settings/utils/zodSchema.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,17 @@ const discourseNodeSettings: DiscourseNodeSettings = {
Confidence: "confidence-attr-uid",
},
overlay: "Status",
index: [
{
type: "filter",
condition: "has attribute",
attribute: "Status",
},
],
index: {
conditions: [
{
type: "clause",
source: "Claim",
relation: "has attribute",
target: "Status",
},
],
selections: [],
},
suggestiveRules: {
template: [],
embeddingRef: "((embed-ref))",
Expand Down
Loading