Skip to content

Commit 2ac769a

Browse files
authored
ENG-1345: Modify existing RoamJS component onChange to also change props (#745)
* address review * address review
1 parent 7769e02 commit 2ac769a

1 file changed

Lines changed: 163 additions & 20 deletions

File tree

apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx

Lines changed: 163 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { type ChangeEvent, useState } from "react";
1+
import React, { type ChangeEvent, useState, useCallback, useRef } from "react";
22
import {
33
Checkbox,
44
InputGroup,
@@ -10,6 +10,8 @@ import {
1010
} from "@blueprintjs/core";
1111
import Description from "roamjs-components/components/Description";
1212
import idToTitle from "roamjs-components/util/idToTitle";
13+
import useSingleChildValue from "roamjs-components/components/ConfigPanels/useSingleChildValue";
14+
import getShallowTreeByParentUid from "roamjs-components/queries/getShallowTreeByParentUid";
1315
import {
1416
getGlobalSetting,
1517
setGlobalSetting,
@@ -20,6 +22,12 @@ import {
2022
} from "~/components/settings/utils/accessors";
2123
import type { FeatureFlags } from "~/components/settings/utils/zodSchema";
2224

25+
type RoamBlockSyncProps = {
26+
parentUid?: string;
27+
uid?: string;
28+
order?: number;
29+
};
30+
2331
type TextGetter = (keys: string[]) => string | undefined;
2432
type TextSetter = (keys: string[], value: string) => void;
2533

@@ -40,7 +48,7 @@ type BaseTextPanelProps = {
4048
setter: TextSetter;
4149
defaultValue?: string;
4250
placeholder?: string;
43-
};
51+
} & RoamBlockSyncProps;
4452

4553
type BaseFlagPanelProps = {
4654
title: string;
@@ -52,7 +60,7 @@ type BaseFlagPanelProps = {
5260
disabled?: boolean;
5361
onBeforeChange?: (checked: boolean) => Promise<boolean>;
5462
onChange?: (checked: boolean) => void;
55-
};
63+
} & RoamBlockSyncProps;
5664

5765
type BaseNumberPanelProps = {
5866
title: string;
@@ -63,7 +71,7 @@ type BaseNumberPanelProps = {
6371
defaultValue?: number;
6472
min?: number;
6573
max?: number;
66-
};
74+
} & RoamBlockSyncProps;
6775

6876
type BaseSelectPanelProps = {
6977
title: string;
@@ -73,7 +81,7 @@ type BaseSelectPanelProps = {
7381
setter: TextSetter;
7482
options: string[];
7583
defaultValue?: string;
76-
};
84+
} & RoamBlockSyncProps;
7785

7886
type BaseMultiTextPanelProps = {
7987
title: string;
@@ -82,8 +90,7 @@ type BaseMultiTextPanelProps = {
8290
getter: MultiTextGetter;
8391
setter: MultiTextSetter;
8492
defaultValue?: string[];
85-
};
86-
93+
} & RoamBlockSyncProps;
8794

8895
const BaseTextPanel = ({
8996
title,
@@ -93,13 +100,28 @@ const BaseTextPanel = ({
93100
setter,
94101
defaultValue = "",
95102
placeholder,
103+
parentUid,
104+
uid,
105+
order,
96106
}: BaseTextPanelProps) => {
97107
const [value, setValue] = useState(() => getter(settingKeys) ?? defaultValue);
108+
const hasBlockSync = parentUid !== undefined && order !== undefined;
109+
const { onChange: rawSyncToBlock } = useSingleChildValue({
110+
title,
111+
parentUid: parentUid ?? "",
112+
order: order ?? 0,
113+
uid,
114+
defaultValue,
115+
transform: (s: string) => s,
116+
toStr: (s: string) => s,
117+
});
118+
const syncToBlock = hasBlockSync ? rawSyncToBlock : undefined;
98119

99120
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
100121
const newValue = e.target.value;
101122
setValue(newValue);
102123
setter(settingKeys, newValue);
124+
syncToBlock?.(newValue);
103125
};
104126

105127
return (
@@ -125,8 +147,35 @@ const BaseFlagPanel = ({
125147
disabled = false,
126148
onBeforeChange,
127149
onChange,
150+
parentUid,
151+
uid: initialBlockUid,
152+
order,
128153
}: BaseFlagPanelProps) => {
129154
const [value, setValue] = useState(() => getter(settingKeys) ?? defaultValue);
155+
const blockUidRef = useRef(initialBlockUid);
156+
157+
const syncFlagToBlock = useCallback(
158+
async (checked: boolean) => {
159+
if (parentUid === undefined || order === undefined) return;
160+
if (checked) {
161+
if (blockUidRef.current) return;
162+
const newUid = window.roamAlphaAPI.util.generateUID();
163+
// eslint-disable-next-line @typescript-eslint/naming-convention
164+
await window.roamAlphaAPI.data.block.create({
165+
block: { string: title, uid: newUid },
166+
// eslint-disable-next-line @typescript-eslint/naming-convention
167+
location: { order, "parent-uid": parentUid },
168+
});
169+
blockUidRef.current = newUid;
170+
} else if (blockUidRef.current) {
171+
await window.roamAlphaAPI.deleteBlock({
172+
block: { uid: blockUidRef.current },
173+
});
174+
blockUidRef.current = undefined;
175+
}
176+
},
177+
[title, parentUid, order],
178+
);
130179

131180
const handleChange = async (e: React.FormEvent<HTMLInputElement>) => {
132181
const { checked } = e.target as HTMLInputElement;
@@ -138,6 +187,7 @@ const BaseFlagPanel = ({
138187

139188
setValue(checked);
140189
setter(settingKeys, checked);
190+
await syncFlagToBlock(checked);
141191
onChange?.(checked);
142192
};
143193

@@ -165,13 +215,28 @@ const BaseNumberPanel = ({
165215
defaultValue = 0,
166216
min,
167217
max,
218+
parentUid,
219+
uid,
220+
order,
168221
}: BaseNumberPanelProps) => {
169222
const [value, setValue] = useState(() => getter(settingKeys) ?? defaultValue);
223+
const hasBlockSync = parentUid !== undefined && order !== undefined;
224+
const { onChange: rawSyncToBlock } = useSingleChildValue({
225+
title,
226+
parentUid: parentUid ?? "",
227+
order: order ?? 0,
228+
uid,
229+
defaultValue,
230+
transform: (s: string) => parseInt(s, 10),
231+
toStr: (v: number) => `${v}`,
232+
});
233+
const syncToBlock = hasBlockSync ? rawSyncToBlock : undefined;
170234

171235
const handleChange = (valueAsNumber: number) => {
172236
if (Number.isNaN(valueAsNumber)) return;
173237
setValue(valueAsNumber);
174238
setter(settingKeys, valueAsNumber);
239+
syncToBlock?.(valueAsNumber);
175240
};
176241

177242
return (
@@ -197,22 +262,42 @@ const BaseSelectPanel = ({
197262
setter,
198263
options,
199264
defaultValue,
265+
parentUid,
266+
uid,
267+
order,
200268
}: BaseSelectPanelProps) => {
201269
const [value, setValue] = useState(
202270
() => getter(settingKeys) ?? defaultValue ?? options[0],
203271
);
272+
const hasBlockSync = parentUid !== undefined && order !== undefined;
273+
const { onChange: rawSyncToBlock } = useSingleChildValue({
274+
title,
275+
parentUid: parentUid ?? "",
276+
order: order ?? 0,
277+
uid,
278+
defaultValue: defaultValue ?? options[0] ?? "",
279+
transform: (s: string) => s,
280+
toStr: (s: string) => s,
281+
});
282+
const syncToBlock = hasBlockSync ? rawSyncToBlock : undefined;
204283

205284
const handleChange = (e: ChangeEvent<HTMLSelectElement>) => {
206285
const newValue = e.target.value;
207286
setValue(newValue);
208287
setter(settingKeys, newValue);
288+
syncToBlock?.(newValue);
209289
};
210290

211291
return (
212292
<Label>
213293
{idToTitle(title)}
214294
<Description description={description} />
215-
<HTMLSelect value={value} onChange={handleChange} fill options={options} />
295+
<HTMLSelect
296+
value={value}
297+
onChange={handleChange}
298+
fill
299+
options={options}
300+
/>
216301
</Label>
217302
);
218303
};
@@ -224,18 +309,60 @@ const BaseMultiTextPanel = ({
224309
getter,
225310
setter,
226311
defaultValue = [],
312+
parentUid,
313+
uid: initialBlockUid,
314+
order,
227315
}: BaseMultiTextPanelProps) => {
228316
const [values, setValues] = useState<string[]>(
229317
() => getter(settingKeys) ?? defaultValue,
230318
);
231319
const [inputValue, setInputValue] = useState("");
320+
const hasBlockSync = parentUid !== undefined && order !== undefined;
321+
const blockUidRef = useRef(initialBlockUid);
322+
const childUidsRef = useRef<string[]>(
323+
initialBlockUid
324+
? getShallowTreeByParentUid(initialBlockUid).map(
325+
(c: { uid: string }) => c.uid,
326+
)
327+
: [],
328+
);
232329

233-
const handleAdd = () => {
330+
const ensureParentBlock = useCallback(async (): Promise<
331+
string | undefined
332+
> => {
333+
if (blockUidRef.current) return blockUidRef.current;
334+
if (parentUid === undefined || order === undefined) return undefined;
335+
const newUid = window.roamAlphaAPI.util.generateUID();
336+
await window.roamAlphaAPI.createBlock({
337+
block: { string: title, uid: newUid },
338+
// eslint-disable-next-line @typescript-eslint/naming-convention
339+
location: { order, "parent-uid": parentUid },
340+
});
341+
blockUidRef.current = newUid;
342+
return newUid;
343+
}, [title, parentUid, order]);
344+
345+
const handleAdd = async () => {
234346
if (inputValue.trim() && !values.includes(inputValue.trim())) {
235-
const newValues = [...values, inputValue.trim()];
347+
const trimmed = inputValue.trim();
348+
const newValues = [...values, trimmed];
236349
setValues(newValues);
237350
setter(settingKeys, newValues);
238351
setInputValue("");
352+
353+
const parent = await ensureParentBlock();
354+
if (parent) {
355+
const valueUid = window.roamAlphaAPI.util.generateUID();
356+
await window.roamAlphaAPI.createBlock({
357+
block: { string: trimmed, uid: valueUid },
358+
location: {
359+
order: childUidsRef.current.length,
360+
// eslint-disable-next-line @typescript-eslint/naming-convention
361+
"parent-uid": parent,
362+
},
363+
});
364+
childUidsRef.current = [...childUidsRef.current, valueUid];
365+
}
239366
}
240367
};
241368

@@ -244,12 +371,23 @@ const BaseMultiTextPanel = ({
244371
const newValues = values.filter((_, i) => i !== index);
245372
setValues(newValues);
246373
setter(settingKeys, newValues);
374+
375+
if (hasBlockSync) {
376+
const removedUid = childUidsRef.current[index];
377+
if (removedUid) {
378+
void window.roamAlphaAPI.deleteBlock({ block: { uid: removedUid } });
379+
}
380+
childUidsRef.current = childUidsRef.current.filter(
381+
// eslint-disable-next-line @typescript-eslint/naming-convention
382+
(_, i) => i !== index,
383+
);
384+
}
247385
};
248386

249387
const handleKeyDown = (e: React.KeyboardEvent) => {
250388
if (e.key === "Enter") {
251389
e.preventDefault();
252-
handleAdd();
390+
void handleAdd();
253391
}
254392
};
255393

@@ -265,7 +403,11 @@ const BaseMultiTextPanel = ({
265403
placeholder="Add new item"
266404
className="flex-grow"
267405
/>
268-
<Button icon="plus" onClick={handleAdd} disabled={!inputValue.trim()} />
406+
<Button
407+
icon="plus"
408+
onClick={() => void handleAdd()}
409+
disabled={!inputValue.trim()}
410+
/>
269411
</div>
270412
{values.length > 0 && (
271413
<div className="mt-2 flex flex-wrap gap-1">
@@ -337,15 +479,16 @@ export const FeatureFlagPanel = ({
337479
onBeforeEnable?: () => Promise<boolean>;
338480
onAfterChange?: (checked: boolean) => void;
339481
}) => {
340-
const handleBeforeChange: ((checked: boolean) => Promise<boolean>) | undefined =
341-
onBeforeEnable
342-
? async (checked) => {
343-
if (checked) {
344-
return onBeforeEnable();
345-
}
346-
return true;
482+
const handleBeforeChange:
483+
| ((checked: boolean) => Promise<boolean>)
484+
| undefined = onBeforeEnable
485+
? async (checked) => {
486+
if (checked) {
487+
return onBeforeEnable();
347488
}
348-
: undefined;
489+
return true;
490+
}
491+
: undefined;
349492

350493
return (
351494
<BaseFlagPanel

0 commit comments

Comments
 (0)