Skip to content

Commit 3fbeb9f

Browse files
committed
ENG-1187: Prod: Implement get and set for block prop based settings
1 parent 76d487b commit 3fbeb9f

File tree

2 files changed

+328
-2
lines changed

2 files changed

+328
-2
lines changed
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import getBlockProps, { type json } from "~/utils/getBlockProps";
2+
import setBlockProps from "~/utils/setBlockProps";
3+
import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage";
4+
import { z } from "zod";
5+
import {
6+
DG_BLOCK_PROP_SETTINGS_PAGE_TITLE,
7+
TOP_LEVEL_BLOCK_PROP_KEYS,
8+
DISCOURSE_NODE_PAGE_PREFIX,
9+
} from "../data/blockPropsSettingsConfig";
10+
import {
11+
FeatureFlagsSchema,
12+
GlobalSettingsSchema,
13+
PersonalSettingsSchema,
14+
DiscourseNodeSchema,
15+
type FeatureFlags,
16+
type GlobalSettings,
17+
type PersonalSettings,
18+
type DiscourseNodeSettings,
19+
} from "./zodSchema";
20+
import {
21+
getPersonalSettingsKey,
22+
getDiscourseNodePageUid,
23+
} from "./init";
24+
25+
const isRecord = (value: unknown): value is Record<string, unknown> =>
26+
typeof value === "object" && value !== null && !Array.isArray(value);
27+
28+
export const getBlockPropsByUid = (
29+
blockUid: string,
30+
keys: string[],
31+
): json | undefined => {
32+
if (!blockUid) return undefined;
33+
34+
const allBlockProps = getBlockProps(blockUid);
35+
36+
if (keys.length === 0) {
37+
return allBlockProps;
38+
}
39+
40+
const targetValue = keys.reduce((currentContext: json, currentKey) => {
41+
if (
42+
currentContext &&
43+
typeof currentContext === "object" &&
44+
!Array.isArray(currentContext)
45+
) {
46+
const value = (currentContext as Record<string, json>)[currentKey];
47+
return value === undefined ? null : value;
48+
}
49+
return null;
50+
}, allBlockProps);
51+
52+
return targetValue === null ? undefined : targetValue;
53+
};
54+
55+
export const setBlockPropsByUid = (
56+
blockUid: string,
57+
keys: string[],
58+
value: json,
59+
): void => {
60+
if (!blockUid) {
61+
console.warn("[DG:accessor] setBlockPropsByUid called with empty blockUid");
62+
return;
63+
}
64+
65+
if (keys.length === 0) {
66+
setBlockProps(blockUid, value as Record<string, json>, false);
67+
return;
68+
}
69+
70+
const currentProps = getBlockProps(blockUid);
71+
const updatedProps = JSON.parse(JSON.stringify(currentProps || {})) as Record<
72+
string,
73+
json
74+
>;
75+
76+
const lastKeyIndex = keys.length - 1;
77+
78+
keys.reduce<Record<string, json>>((currentContext, currentKey, index) => {
79+
if (index === lastKeyIndex) {
80+
currentContext[currentKey] = value;
81+
return currentContext;
82+
}
83+
84+
if (
85+
!currentContext[currentKey] ||
86+
typeof currentContext[currentKey] !== "object" ||
87+
Array.isArray(currentContext[currentKey])
88+
) {
89+
currentContext[currentKey] = {};
90+
}
91+
92+
return currentContext[currentKey] as Record<string, json>;
93+
}, updatedProps);
94+
95+
setBlockProps(blockUid, updatedProps, false);
96+
};
97+
98+
export const getBlockPropBasedSettings = ({
99+
keys,
100+
}: {
101+
keys: string[];
102+
}): { blockProps: json | undefined; blockUid: string } => {
103+
if (keys.length === 0) {
104+
console.warn("[DG:accessor] getBlockPropBasedSettings called with no keys");
105+
return { blockProps: undefined, blockUid: "" };
106+
}
107+
108+
const blockUid = getBlockUidByTextOnPage({
109+
text: keys[0],
110+
title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE,
111+
});
112+
113+
if (!blockUid) {
114+
return { blockProps: undefined, blockUid: "" };
115+
}
116+
117+
const blockProps = getBlockPropsByUid(blockUid, keys.slice(1));
118+
119+
return { blockProps, blockUid };
120+
};
121+
122+
export const setBlockPropBasedSettings = ({
123+
keys,
124+
value,
125+
}: {
126+
keys: string[];
127+
value: json;
128+
}): void => {
129+
if (keys.length === 0) {
130+
console.warn("[DG:accessor] setBlockPropBasedSettings called with no keys");
131+
return;
132+
}
133+
134+
const blockUid = getBlockUidByTextOnPage({
135+
text: keys[0],
136+
title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE,
137+
});
138+
139+
if (!blockUid) {
140+
console.warn(
141+
`[DG:accessor] Block not found for key "${keys[0]}" on settings page`,
142+
);
143+
return;
144+
}
145+
146+
setBlockPropsByUid(blockUid, keys.slice(1), value);
147+
};
148+
149+
export const getFeatureFlags = (): FeatureFlags => {
150+
const { blockProps } = getBlockPropBasedSettings({
151+
keys: [TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags],
152+
});
153+
154+
return FeatureFlagsSchema.parse(blockProps || {});
155+
};
156+
157+
export const getFeatureFlag = (key: keyof FeatureFlags): boolean => {
158+
const flags = getFeatureFlags();
159+
return flags[key];
160+
};
161+
162+
export const setFeatureFlag = (
163+
key: keyof FeatureFlags,
164+
value: boolean,
165+
): void => {
166+
const validatedValue = z.boolean().parse(value);
167+
168+
setBlockPropBasedSettings({
169+
keys: [TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags, key],
170+
value: validatedValue,
171+
});
172+
};
173+
174+
export const getGlobalSettings = (): GlobalSettings => {
175+
const { blockProps } = getBlockPropBasedSettings({
176+
keys: [TOP_LEVEL_BLOCK_PROP_KEYS.global],
177+
});
178+
179+
return GlobalSettingsSchema.parse(blockProps || {});
180+
};
181+
182+
export const getGlobalSetting = <T = unknown>(keys: string[]): T | undefined => {
183+
const settings = getGlobalSettings();
184+
185+
return keys.reduce<unknown>((current, key) => {
186+
if (!isRecord(current) || !(key in current)) return undefined;
187+
return current[key];
188+
}, settings) as T | undefined;
189+
};
190+
191+
export const setGlobalSetting = (keys: string[], value: json): void => {
192+
setBlockPropBasedSettings({
193+
keys: [TOP_LEVEL_BLOCK_PROP_KEYS.global, ...keys],
194+
value,
195+
});
196+
};
197+
198+
export const getPersonalSettings = (): PersonalSettings => {
199+
const personalKey = getPersonalSettingsKey();
200+
201+
const { blockProps } = getBlockPropBasedSettings({
202+
keys: [personalKey],
203+
});
204+
205+
return PersonalSettingsSchema.parse(blockProps || {});
206+
};
207+
208+
export const getPersonalSetting = <T = unknown>(
209+
keys: string[],
210+
): T | undefined => {
211+
const settings = getPersonalSettings();
212+
213+
return keys.reduce<unknown>((current, key) => {
214+
if (!isRecord(current) || !(key in current)) return undefined;
215+
return current[key];
216+
}, settings) as T | undefined;
217+
};
218+
219+
export const setPersonalSetting = (keys: string[], value: json): void => {
220+
const personalKey = getPersonalSettingsKey();
221+
222+
setBlockPropBasedSettings({
223+
keys: [personalKey, ...keys],
224+
value,
225+
});
226+
};
227+
228+
export const getDiscourseNodeSettings = (
229+
nodeType: string,
230+
): DiscourseNodeSettings | undefined => {
231+
let pageUid = nodeType;
232+
let blockProps = getBlockPropsByUid(pageUid, []);
233+
234+
if (!blockProps || Object.keys(blockProps).length === 0) {
235+
const lookedUpUid = getDiscourseNodePageUid(nodeType);
236+
if (lookedUpUid) {
237+
pageUid = lookedUpUid;
238+
blockProps = getBlockPropsByUid(pageUid, []);
239+
}
240+
}
241+
242+
if (!blockProps) return undefined;
243+
244+
const result = DiscourseNodeSchema.safeParse(blockProps);
245+
if (!result.success) {
246+
console.warn(
247+
`[DG:accessor] Failed to parse discourse node settings for ${nodeType}:`,
248+
result.error,
249+
);
250+
return undefined;
251+
}
252+
253+
return result.data;
254+
};
255+
256+
export const getDiscourseNodeSetting = <T = unknown>(
257+
nodeType: string,
258+
keys: string[],
259+
): T | undefined => {
260+
const settings = getDiscourseNodeSettings(nodeType);
261+
262+
if (!settings) return undefined;
263+
264+
return keys.reduce<unknown>((current, key) => {
265+
if (!isRecord(current) || !(key in current)) return undefined;
266+
return current[key];
267+
}, settings) as T | undefined;
268+
};
269+
270+
export const setDiscourseNodeSetting = (
271+
nodeType: string,
272+
keys: string[],
273+
value: json,
274+
): void => {
275+
let pageUid = nodeType;
276+
let blockProps = getBlockPropsByUid(pageUid, []);
277+
278+
if (!blockProps || Object.keys(blockProps).length === 0) {
279+
const lookedUpUid = getDiscourseNodePageUid(nodeType);
280+
if (lookedUpUid) {
281+
pageUid = lookedUpUid;
282+
}
283+
}
284+
285+
if (!pageUid) {
286+
console.warn(
287+
`[DG:accessor] setDiscourseNodeSetting - could not find page for: ${nodeType}`,
288+
);
289+
return;
290+
}
291+
292+
setBlockPropsByUid(pageUid, keys, value);
293+
};
294+
295+
export const getAllDiscourseNodes = (): DiscourseNodeSettings[] => {
296+
const results = window.roamAlphaAPI.q(`
297+
[:find ?uid ?title
298+
:where
299+
[?page :node/title ?title]
300+
[?page :block/uid ?uid]
301+
[(clojure.string/starts-with? ?title "${DISCOURSE_NODE_PAGE_PREFIX}")]]
302+
`) as [string, string][];
303+
304+
const nodes: DiscourseNodeSettings[] = [];
305+
306+
for (const [pageUid, title] of results) {
307+
const blockProps = getBlockPropsByUid(pageUid, []);
308+
if (!blockProps) continue;
309+
310+
const result = DiscourseNodeSchema.safeParse(blockProps);
311+
if (result.success) {
312+
nodes.push({
313+
...result.data,
314+
type: pageUid,
315+
text: title.replace(DISCOURSE_NODE_PAGE_PREFIX, ""),
316+
});
317+
}
318+
}
319+
320+
return nodes;
321+
};

apps/roam/src/components/settings/utils/init.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,23 @@ import {
2121

2222
let cachedPersonalSettingsKey: string | null = null;
2323

24-
const getPersonalSettingsKey = (): string => {
24+
export const getPersonalSettingsKey = (): string => {
2525
if (cachedPersonalSettingsKey !== null) {
2626
return cachedPersonalSettingsKey;
2727
}
2828
cachedPersonalSettingsKey = window.roamAlphaAPI.user.uid() || "";
2929
return cachedPersonalSettingsKey;
3030
};
3131

32-
const getDiscourseNodePageTitle = (nodeLabel: string): string => {
32+
export const getDiscourseNodePageTitle = (nodeLabel: string): string => {
3333
return `${DISCOURSE_NODE_PAGE_PREFIX}${nodeLabel}`;
3434
};
3535

36+
export const getDiscourseNodePageUid = (nodeLabel: string): string => {
37+
const pageTitle = getDiscourseNodePageTitle(nodeLabel);
38+
return getPageUidByPageTitle(pageTitle);
39+
};
40+
3641
const ensurePageExists = async (pageTitle: string): Promise<string> => {
3742
let pageUid = getPageUidByPageTitle(pageTitle);
3843

0 commit comments

Comments
 (0)