Skip to content

Commit 0962e48

Browse files
committed
pull watchers for prop settings
1 parent 3d7e4c7 commit 0962e48

File tree

1 file changed

+276
-0
lines changed

1 file changed

+276
-0
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import { type json, normalizeProps } from "~/utils/getBlockProps";
2+
import {
3+
TOP_LEVEL_BLOCK_PROP_KEYS,
4+
DISCOURSE_NODE_PAGE_PREFIX,
5+
} from "../data/blockPropsSettingsConfig";
6+
import { getPersonalSettingsKey } from "./init";
7+
import {
8+
FeatureFlagsSchema,
9+
GlobalSettingsSchema,
10+
PersonalSettingsSchema,
11+
DiscourseNodeSchema,
12+
type FeatureFlags,
13+
type GlobalSettings,
14+
type PersonalSettings,
15+
type DiscourseNodeSettings,
16+
} from "./zodSchema";
17+
18+
type PullWatchCallback = (before: unknown, after: unknown) => void;
19+
20+
type PullWatchEntry = {
21+
pattern: string;
22+
entityId: string;
23+
callback: PullWatchCallback;
24+
};
25+
26+
const getNormalizedProps = (data: unknown): Record<string, json> => {
27+
return normalizeProps(
28+
((data as Record<string, unknown>)?.[":block/props"] || {}) as json,
29+
) as Record<string, json>;
30+
};
31+
32+
const hasPropChanged = (
33+
before: unknown,
34+
after: unknown,
35+
key?: string,
36+
): boolean => {
37+
const beforeProps = getNormalizedProps(before);
38+
const afterProps = getNormalizedProps(after);
39+
40+
if (key) {
41+
return JSON.stringify(beforeProps[key]) !== JSON.stringify(afterProps[key]);
42+
}
43+
44+
return JSON.stringify(beforeProps) !== JSON.stringify(afterProps);
45+
};
46+
47+
const createCleanupFn = (watches: PullWatchEntry[]): (() => void) => {
48+
return () => {
49+
watches.forEach(({ pattern, entityId, callback }) => {
50+
window.roamAlphaAPI.data.removePullWatch(pattern, entityId, callback);
51+
});
52+
};
53+
};
54+
55+
const addPullWatch = (
56+
watches: PullWatchEntry[],
57+
blockUid: string,
58+
callback: PullWatchCallback,
59+
): void => {
60+
const pattern = "[:block/props]";
61+
const entityId = `[:block/uid "${blockUid}"]`;
62+
63+
window.roamAlphaAPI.data.addPullWatch(pattern, entityId, callback);
64+
watches.push({ pattern, entityId, callback });
65+
};
66+
67+
type FeatureFlagHandler = (
68+
newValue: boolean,
69+
oldValue: boolean,
70+
allSettings: FeatureFlags,
71+
) => void;
72+
73+
type GlobalSettingHandler<K extends keyof GlobalSettings = keyof GlobalSettings> = (
74+
newValue: GlobalSettings[K],
75+
oldValue: GlobalSettings[K],
76+
allSettings: GlobalSettings,
77+
) => void;
78+
79+
type PersonalSettingHandler<K extends keyof PersonalSettings = keyof PersonalSettings> = (
80+
newValue: PersonalSettings[K],
81+
oldValue: PersonalSettings[K],
82+
allSettings: PersonalSettings,
83+
) => void;
84+
85+
type DiscourseNodeHandler = (
86+
nodeType: string,
87+
newSettings: DiscourseNodeSettings,
88+
oldSettings: DiscourseNodeSettings | null,
89+
) => void;
90+
91+
export const featureFlagHandlers: Partial<
92+
Record<keyof FeatureFlags, FeatureFlagHandler>
93+
> = {
94+
// Add handlers as needed:
95+
// "Enable Left Sidebar": (newValue) => { ... },
96+
// "Suggestive Mode Enabled": (newValue) => { ... },
97+
// "Reified Relation Triples": (newValue) => { ... },
98+
};
99+
100+
export const globalSettingsHandlers: Partial<
101+
Record<keyof GlobalSettings, GlobalSettingHandler>
102+
> = {
103+
// Add handlers as needed:
104+
// "Trigger": (newValue) => { ... },
105+
// "Canvas Page Format": (newValue) => { ... },
106+
// "Left Sidebar": (newValue) => { ... },
107+
// "Export": (newValue) => { ... },
108+
// "Suggestive Mode": (newValue) => { ... },
109+
};
110+
111+
export const personalSettingsHandlers: Partial<
112+
Record<keyof PersonalSettings, PersonalSettingHandler>
113+
> = {
114+
// Add handlers as needed:
115+
// "Left Sidebar": (newValue) => { ... },
116+
// "Discourse Context Overlay": (newValue) => { ... },
117+
// "Page Preview": (newValue) => { ... },
118+
// etc.
119+
};
120+
121+
122+
export const discourseNodeHandlers: DiscourseNodeHandler[] = [
123+
// Add handlers as needed:
124+
// (nodeType, newSettings, oldSettings) => { ... },
125+
];
126+
127+
128+
export const setupPullWatchSettings = (
129+
blockUids: Record<string, string>,
130+
): (() => void) => {
131+
const watches: PullWatchEntry[] = [];
132+
133+
const featureFlagsBlockUid =
134+
blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags];
135+
const globalSettingsBlockUid = blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.global];
136+
const personalSettingsKey = getPersonalSettingsKey();
137+
const personalSettingsBlockUid = blockUids[personalSettingsKey];
138+
139+
if (featureFlagsBlockUid && Object.keys(featureFlagHandlers).length > 0) {
140+
addPullWatch(watches, featureFlagsBlockUid, (before, after) => {
141+
if (!hasPropChanged(before, after)) return;
142+
143+
const beforeProps = getNormalizedProps(before);
144+
const afterProps = getNormalizedProps(after);
145+
const beforeResult = FeatureFlagsSchema.safeParse(beforeProps);
146+
const afterResult = FeatureFlagsSchema.safeParse(afterProps);
147+
148+
if (!afterResult.success) return;
149+
150+
const oldSettings = beforeResult.success ? beforeResult.data : null;
151+
const newSettings = afterResult.data;
152+
153+
for (const [key, handler] of Object.entries(featureFlagHandlers)) {
154+
const typedKey = key as keyof FeatureFlags;
155+
if (hasPropChanged(before, after, key) && handler) {
156+
handler(
157+
newSettings[typedKey],
158+
oldSettings?.[typedKey] ?? false,
159+
newSettings,
160+
);
161+
}
162+
}
163+
});
164+
}
165+
166+
if (globalSettingsBlockUid && Object.keys(globalSettingsHandlers).length > 0) {
167+
addPullWatch(watches, globalSettingsBlockUid, (before, after) => {
168+
if (!hasPropChanged(before, after)) return;
169+
170+
const beforeProps = getNormalizedProps(before);
171+
const afterProps = getNormalizedProps(after);
172+
const beforeResult = GlobalSettingsSchema.safeParse(beforeProps);
173+
const afterResult = GlobalSettingsSchema.safeParse(afterProps);
174+
175+
if (!afterResult.success) return;
176+
177+
const oldSettings = beforeResult.success ? beforeResult.data : null;
178+
const newSettings = afterResult.data;
179+
180+
for (const [key, handler] of Object.entries(globalSettingsHandlers)) {
181+
const typedKey = key as keyof GlobalSettings;
182+
if (hasPropChanged(before, after, key) && handler) {
183+
handler(
184+
newSettings[typedKey],
185+
oldSettings?.[typedKey] as GlobalSettings[typeof typedKey],
186+
newSettings,
187+
);
188+
}
189+
}
190+
});
191+
}
192+
193+
if (personalSettingsBlockUid && Object.keys(personalSettingsHandlers).length > 0) {
194+
addPullWatch(watches, personalSettingsBlockUid, (before, after) => {
195+
if (!hasPropChanged(before, after)) return;
196+
197+
const beforeProps = getNormalizedProps(before);
198+
const afterProps = getNormalizedProps(after);
199+
const beforeResult = PersonalSettingsSchema.safeParse(beforeProps);
200+
const afterResult = PersonalSettingsSchema.safeParse(afterProps);
201+
202+
if (!afterResult.success) return;
203+
204+
const oldSettings = beforeResult.success ? beforeResult.data : null;
205+
const newSettings = afterResult.data;
206+
207+
for (const [key, handler] of Object.entries(personalSettingsHandlers)) {
208+
const typedKey = key as keyof PersonalSettings;
209+
if (hasPropChanged(before, after, key) && handler) {
210+
handler(
211+
newSettings[typedKey],
212+
oldSettings?.[typedKey] as PersonalSettings[typeof typedKey],
213+
newSettings,
214+
);
215+
}
216+
}
217+
});
218+
}
219+
220+
return createCleanupFn(watches);
221+
};
222+
223+
224+
export const setupPullWatchDiscourseNodes = (
225+
nodePageUids: Record<string, string>,
226+
): (() => void) => {
227+
const watches: PullWatchEntry[] = [];
228+
229+
if (discourseNodeHandlers.length === 0) {
230+
return () => {};
231+
}
232+
233+
Object.entries(nodePageUids).forEach(([nodeType, pageUid]) => {
234+
addPullWatch(watches, pageUid, (before, after) => {
235+
if (!hasPropChanged(before, after)) return;
236+
237+
const beforeProps = getNormalizedProps(before);
238+
const afterProps = getNormalizedProps(after);
239+
const beforeResult = DiscourseNodeSchema.safeParse(beforeProps);
240+
const afterResult = DiscourseNodeSchema.safeParse(afterProps);
241+
242+
if (!afterResult.success) return;
243+
244+
const oldSettings = beforeResult.success ? beforeResult.data : null;
245+
const newSettings = afterResult.data;
246+
247+
for (const handler of discourseNodeHandlers) {
248+
handler(nodeType, newSettings, oldSettings);
249+
}
250+
});
251+
});
252+
253+
return createCleanupFn(watches);
254+
};
255+
256+
257+
export const queryAllDiscourseNodePageUids = (): Record<string, string> => {
258+
const results = window.roamAlphaAPI.q(`
259+
[:find ?uid ?title
260+
:where
261+
[?page :node/title ?title]
262+
[?page :block/uid ?uid]
263+
[(clojure.string/starts-with? ?title "${DISCOURSE_NODE_PAGE_PREFIX}")]]
264+
`) as [string, string][];
265+
266+
const nodePageUids: Record<string, string> = {};
267+
268+
for (const [pageUid, title] of results) {
269+
const nodeLabel = title.replace(DISCOURSE_NODE_PAGE_PREFIX, "");
270+
nodePageUids[nodeLabel] = pageUid;
271+
}
272+
273+
return nodePageUids;
274+
};
275+
276+
export { hasPropChanged, getNormalizedProps };

0 commit comments

Comments
 (0)