1- import React , { type ChangeEvent , useState } from "react" ;
1+ import React , { type ChangeEvent , useState , useCallback , useRef } from "react" ;
22import {
33 Checkbox ,
44 InputGroup ,
@@ -10,6 +10,8 @@ import {
1010} from "@blueprintjs/core" ;
1111import Description from "roamjs-components/components/Description" ;
1212import idToTitle from "roamjs-components/util/idToTitle" ;
13+ import useSingleChildValue from "roamjs-components/components/ConfigPanels/useSingleChildValue" ;
14+ import getShallowTreeByParentUid from "roamjs-components/queries/getShallowTreeByParentUid" ;
1315import {
1416 getGlobalSetting ,
1517 setGlobalSetting ,
@@ -20,6 +22,12 @@ import {
2022} from "~/components/settings/utils/accessors" ;
2123import type { FeatureFlags } from "~/components/settings/utils/zodSchema" ;
2224
25+ type RoamBlockSyncProps = {
26+ parentUid ?: string ;
27+ uid ?: string ;
28+ order ?: number ;
29+ } ;
30+
2331type TextGetter = ( keys : string [ ] ) => string | undefined ;
2432type 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
4553type 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
5765type BaseNumberPanelProps = {
5866 title : string ;
@@ -63,7 +71,7 @@ type BaseNumberPanelProps = {
6371 defaultValue ?: number ;
6472 min ?: number ;
6573 max ?: number ;
66- } ;
74+ } & RoamBlockSyncProps ;
6775
6876type BaseSelectPanelProps = {
6977 title : string ;
@@ -73,7 +81,7 @@ type BaseSelectPanelProps = {
7381 setter : TextSetter ;
7482 options : string [ ] ;
7583 defaultValue ?: string ;
76- } ;
84+ } & RoamBlockSyncProps ;
7785
7886type BaseMultiTextPanelProps = {
7987 title : string ;
@@ -82,8 +90,7 @@ type BaseMultiTextPanelProps = {
8290 getter : MultiTextGetter ;
8391 setter : MultiTextSetter ;
8492 defaultValue ?: string [ ] ;
85- } ;
86-
93+ } & RoamBlockSyncProps ;
8794
8895const 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