Skip to content
Open
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
321 changes: 321 additions & 0 deletions apps/roam/src/components/settings/utils/accessors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
import getBlockProps, { type json } from "~/utils/getBlockProps";
import setBlockProps from "~/utils/setBlockProps";
import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage";
import { z } from "zod";
import {
DG_BLOCK_PROP_SETTINGS_PAGE_TITLE,
TOP_LEVEL_BLOCK_PROP_KEYS,
DISCOURSE_NODE_PAGE_PREFIX,
} from "../data/blockPropsSettingsConfig";
import {
FeatureFlagsSchema,
GlobalSettingsSchema,
PersonalSettingsSchema,
DiscourseNodeSchema,
type FeatureFlags,
type GlobalSettings,
type PersonalSettings,
type DiscourseNodeSettings,
} from "./zodSchema";
import {
getPersonalSettingsKey,
getDiscourseNodePageUid,
} from "./init";

const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);

export const getBlockPropsByUid = (
blockUid: string,
keys: string[],
): json | undefined => {
if (!blockUid) return undefined;

const allBlockProps = getBlockProps(blockUid);

if (keys.length === 0) {
return allBlockProps;
}

const targetValue = keys.reduce((currentContext: json, currentKey) => {
if (
currentContext &&
typeof currentContext === "object" &&
!Array.isArray(currentContext)
) {
const value = (currentContext as Record<string, json>)[currentKey];
return value === undefined ? null : value;
}
return null;
}, allBlockProps);

return targetValue === null ? undefined : targetValue;
};

export const setBlockPropsByUid = (
blockUid: string,
keys: string[],
value: json,
): void => {
if (!blockUid) {
console.warn("[DG:accessor] setBlockPropsByUid called with empty blockUid");
return;
}

if (keys.length === 0) {
setBlockProps(blockUid, value as Record<string, json>, false);
return;
}

const currentProps = getBlockProps(blockUid);
const updatedProps = JSON.parse(JSON.stringify(currentProps || {})) as Record<
string,
json
>;

const lastKeyIndex = keys.length - 1;

keys.reduce<Record<string, json>>((currentContext, currentKey, index) => {
if (index === lastKeyIndex) {
currentContext[currentKey] = value;
return currentContext;
}

if (
!currentContext[currentKey] ||
typeof currentContext[currentKey] !== "object" ||
Array.isArray(currentContext[currentKey])
) {
currentContext[currentKey] = {};
}

return currentContext[currentKey] as Record<string, json>;
}, updatedProps);

setBlockProps(blockUid, updatedProps, false);
};

export const getBlockPropBasedSettings = ({
keys,
}: {
keys: string[];
}): { blockProps: json | undefined; blockUid: string } => {
if (keys.length === 0) {
console.warn("[DG:accessor] getBlockPropBasedSettings called with no keys");
return { blockProps: undefined, blockUid: "" };
}

const blockUid = getBlockUidByTextOnPage({
text: keys[0],
title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE,
});

if (!blockUid) {
return { blockProps: undefined, blockUid: "" };
}

const blockProps = getBlockPropsByUid(blockUid, keys.slice(1));

return { blockProps, blockUid };
};

export const setBlockPropBasedSettings = ({
keys,
value,
}: {
keys: string[];
value: json;
}): void => {
if (keys.length === 0) {
console.warn("[DG:accessor] setBlockPropBasedSettings called with no keys");
return;
}

const blockUid = getBlockUidByTextOnPage({
text: keys[0],
title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE,
});

if (!blockUid) {
console.warn(
`[DG:accessor] Block not found for key "${keys[0]}" on settings page`,
);
return;
}

setBlockPropsByUid(blockUid, keys.slice(1), value);
};

export const getFeatureFlags = (): FeatureFlags => {
const { blockProps } = getBlockPropBasedSettings({
keys: [TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags],
});

return FeatureFlagsSchema.parse(blockProps || {});
};

export const getFeatureFlag = (key: keyof FeatureFlags): boolean => {
const flags = getFeatureFlags();
return flags[key];
};

export const setFeatureFlag = (
key: keyof FeatureFlags,
value: boolean,
): void => {
const validatedValue = z.boolean().parse(value);

setBlockPropBasedSettings({
keys: [TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags, key],
value: validatedValue,
});
};

export const getGlobalSettings = (): GlobalSettings => {
const { blockProps } = getBlockPropBasedSettings({
keys: [TOP_LEVEL_BLOCK_PROP_KEYS.global],
});

return GlobalSettingsSchema.parse(blockProps || {});
};

export const getGlobalSetting = <T = unknown>(keys: string[]): T | undefined => {
const settings = getGlobalSettings();

return keys.reduce<unknown>((current, key) => {
if (!isRecord(current) || !(key in current)) return undefined;
return current[key];
}, settings) as T | undefined;
};

export const setGlobalSetting = (keys: string[], value: json): void => {
setBlockPropBasedSettings({
keys: [TOP_LEVEL_BLOCK_PROP_KEYS.global, ...keys],
value,
});
};

export const getPersonalSettings = (): PersonalSettings => {
const personalKey = getPersonalSettingsKey();

const { blockProps } = getBlockPropBasedSettings({
keys: [personalKey],
});

return PersonalSettingsSchema.parse(blockProps || {});
};

export const getPersonalSetting = <T = unknown>(
keys: string[],
): T | undefined => {
const settings = getPersonalSettings();

return keys.reduce<unknown>((current, key) => {
if (!isRecord(current) || !(key in current)) return undefined;
return current[key];
}, settings) as T | undefined;
};

export const setPersonalSetting = (keys: string[], value: json): void => {
const personalKey = getPersonalSettingsKey();

setBlockPropBasedSettings({
keys: [personalKey, ...keys],
value,
});
};

export const getDiscourseNodeSettings = (
nodeType: string,
): DiscourseNodeSettings | undefined => {
let pageUid = nodeType;
let blockProps = getBlockPropsByUid(pageUid, []);

if (!blockProps || Object.keys(blockProps).length === 0) {
const lookedUpUid = getDiscourseNodePageUid(nodeType);
if (lookedUpUid) {
pageUid = lookedUpUid;
blockProps = getBlockPropsByUid(pageUid, []);
}
}

if (!blockProps) return undefined;

const result = DiscourseNodeSchema.safeParse(blockProps);
if (!result.success) {
console.warn(
`[DG:accessor] Failed to parse discourse node settings for ${nodeType}:`,
result.error,
);
return undefined;
}

return result.data;
};

export const getDiscourseNodeSetting = <T = unknown>(
nodeType: string,
keys: string[],
): T | undefined => {
const settings = getDiscourseNodeSettings(nodeType);

if (!settings) return undefined;

return keys.reduce<unknown>((current, key) => {
if (!isRecord(current) || !(key in current)) return undefined;
return current[key];
}, settings) as T | undefined;
};

export const setDiscourseNodeSetting = (
nodeType: string,
keys: string[],
value: json,
): void => {
let pageUid = nodeType;
let blockProps = getBlockPropsByUid(pageUid, []);

if (!blockProps || Object.keys(blockProps).length === 0) {
const lookedUpUid = getDiscourseNodePageUid(nodeType);
if (lookedUpUid) {
pageUid = lookedUpUid;
}
}

if (!pageUid) {
console.warn(
`[DG:accessor] setDiscourseNodeSetting - could not find page for: ${nodeType}`,
);
return;
}

setBlockPropsByUid(pageUid, keys, value);
};

export const getAllDiscourseNodes = (): DiscourseNodeSettings[] => {
const results = window.roamAlphaAPI.q(`
[:find ?uid ?title
:where
[?page :node/title ?title]
[?page :block/uid ?uid]
[(clojure.string/starts-with? ?title "${DISCOURSE_NODE_PAGE_PREFIX}")]]
`) as [string, string][];

const nodes: DiscourseNodeSettings[] = [];

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

const result = DiscourseNodeSchema.safeParse(blockProps);
if (result.success) {
nodes.push({
...result.data,
type: pageUid,
text: title.replace(DISCOURSE_NODE_PAGE_PREFIX, ""),
});
}
}

return nodes;
};
9 changes: 7 additions & 2 deletions apps/roam/src/components/settings/utils/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,23 @@ import {

let cachedPersonalSettingsKey: string | null = null;

const getPersonalSettingsKey = (): string => {
export const getPersonalSettingsKey = (): string => {
if (cachedPersonalSettingsKey !== null) {
return cachedPersonalSettingsKey;
}
cachedPersonalSettingsKey = window.roamAlphaAPI.user.uid() || "";
return cachedPersonalSettingsKey;
};

const getDiscourseNodePageTitle = (nodeLabel: string): string => {
export const getDiscourseNodePageTitle = (nodeLabel: string): string => {
return `${DISCOURSE_NODE_PAGE_PREFIX}${nodeLabel}`;
};

export const getDiscourseNodePageUid = (nodeLabel: string): string => {
const pageTitle = getDiscourseNodePageTitle(nodeLabel);
return getPageUidByPageTitle(pageTitle);
};

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

Expand Down
Loading