1- import React from 'react' ;
1+ import React , { useEffect , useState } from 'react' ;
22import {
3- Alert ,
43 Modal ,
54 Pressable ,
65 ScrollView ,
76 StyleSheet ,
87 Text ,
98 TextInput ,
109 View ,
10+ Alert ,
1111} from 'react-native' ;
12- import { useForm , Controller , type FieldValues } from 'react-hook-form' ;
13- import { validate } from 'superstruct' ;
12+ import { type Struct , validate } from 'superstruct' ;
1413import { useAppTheme } from '../../hooks/useAppTheme' ;
14+ import {
15+ formatSuperstructError ,
16+ parseWithUndefined ,
17+ stringifyWithUndefined ,
18+ } from './utils' ;
1519
1620type Props < T > = {
1721 visible : boolean ;
1822 title : string ;
1923 initialData : T ;
2024 onClose : ( ) => void ;
2125 onSave : ( updated : T ) => void ;
22- validator ?: any ;
26+ validator ?: Struct < T , any > ;
2327} ;
2428
25- export default function MapConfigDialog < T extends FieldValues > ( {
29+ export default function MapConfigDialog < T > ( {
2630 visible,
2731 title,
2832 initialData,
@@ -32,74 +36,94 @@ export default function MapConfigDialog<T extends FieldValues>({
3236} : Props < T > ) {
3337 const theme = useAppTheme ( ) ;
3438 const styles = getThemedStyles ( theme ) ;
35- const { control, handleSubmit, reset } = useForm < T > ( ) ;
3639
37- const onSubmit = ( data : T ) => {
38- if ( validator ) {
39- const [ err , value ] = validate ( data , validator ) ;
40- if ( err ) {
41- Alert . alert (
42- 'Input error' ,
43- `${ err . path ?. join ( '.' ) || '(root)' } : ${ err . message } `
44- ) ;
45- return ;
40+ const [ text , setText ] = useState ( ( ) => stringifyWithUndefined ( initialData ) ) ;
41+ const [ isValid , setIsValid ] = useState ( true ) ;
42+ const [ error , setError ] = useState < string | null > ( null ) ;
43+
44+ useEffect ( ( ) => {
45+ setText ( stringifyWithUndefined ( initialData ) ) ;
46+ setIsValid ( true ) ;
47+ setError ( null ) ;
48+ } , [ initialData ] ) ;
49+
50+ const handleChange = ( value : string ) => {
51+ setText ( value ) ;
52+ try {
53+ const parsed = parseWithUndefined ( value ) ;
54+
55+ if ( validator ) {
56+ const [ err ] = validate ( parsed , validator ) ;
57+ if ( err ) {
58+ setIsValid ( false ) ;
59+ setError ( formatSuperstructError ( err , validator ) ) ;
60+ return ;
61+ }
4662 }
47- onSave ( value as T ) ;
48- } else {
49- onSave ( data ) ;
63+
64+ setIsValid ( true ) ;
65+ setError ( null ) ;
66+ } catch ( e : any ) {
67+ setIsValid ( false ) ;
68+ setError ( e . message ) ;
5069 }
51- onClose ( ) ;
5270 } ;
5371
54- React . useEffect ( ( ) => {
55- reset ( initialData ) ;
56- } , [ initialData , reset ] ) ;
72+ const handleSave = ( ) => {
73+ if ( ! isValid ) {
74+ Alert . alert ( 'Invalid JSON' , error ?? 'Please fix JSON before saving.' ) ;
75+ return ;
76+ }
77+
78+ try {
79+ const parsed = parseWithUndefined ( text ) ;
80+
81+ if ( validator ) {
82+ const [ err , value ] = validate ( parsed , validator ) ;
83+ if ( err ) {
84+ Alert . alert (
85+ 'Validation Error' ,
86+ formatSuperstructError ( err , validator )
87+ ) ;
88+ return ;
89+ }
90+ onSave ( value as T ) ;
91+ } else {
92+ onSave ( parsed as T ) ;
93+ }
94+
95+ onClose ( ) ;
96+ } catch ( e : any ) {
97+ Alert . alert ( 'Invalid JSON' , e . message ) ;
98+ }
99+ } ;
57100
58101 return (
59102 < Modal visible = { visible } transparent animationType = "fade" >
60103 < View style = { styles . overlay } >
61104 < View style = { styles . dialog } >
62105 < Text style = { styles . title } > { title } </ Text >
63106 < ScrollView contentContainerStyle = { styles . scroll } >
64- { Object . entries ( initialData ) . map ( ( [ key , _ ] ) => (
65- < View key = { key } style = { styles . field } >
66- < Text style = { styles . label } > { key } </ Text >
67- < Controller
68- control = { control }
69- name = { key as any }
70- render = { ( { field : { onChange, value } } ) => (
71- < TextInput
72- style = { styles . input }
73- spellCheck = { false }
74- autoCapitalize = { 'none' }
75- autoCorrect = { false }
76- value = {
77- typeof value === 'object'
78- ? JSON . stringify ( value , null , 2 )
79- : String ( value ?? '' )
80- }
81- onChangeText = { ( v ) => {
82- try {
83- onChange ( JSON . parse ( v ) ) ;
84- } catch {
85- onChange ( v ) ;
86- }
87- } }
88- multiline = { typeof value === 'object' }
89- />
90- ) }
91- />
92- </ View >
93- ) ) }
107+ < TextInput
108+ value = { text }
109+ onChangeText = { handleChange }
110+ multiline
111+ style = { [ styles . input , styles . multiline , ! isValid && styles . error ] }
112+ autoCorrect = { false }
113+ autoCapitalize = "none"
114+ spellCheck = { false }
115+ />
116+ { ! isValid && (
117+ < Text style = { [ styles . errorText ] } >
118+ { error ?? 'Invalid JSON or schema mismatch' }
119+ </ Text >
120+ ) }
94121 </ ScrollView >
95122 < View style = { styles . actions } >
96123 < Pressable onPress = { onClose } style = { styles . cancelButton } >
97124 < Text style = { styles . buttonText } > Cancel</ Text >
98125 </ Pressable >
99- < Pressable
100- onPress = { handleSubmit ( onSubmit ) }
101- style = { styles . saveButton }
102- >
126+ < Pressable onPress = { handleSave } style = { styles . saveButton } >
103127 < Text style = { styles . buttonText } > Save</ Text >
104128 </ Pressable >
105129 </ View >
@@ -122,16 +146,15 @@ const getThemedStyles = (theme: any) =>
122146 maxHeight : '85%' ,
123147 backgroundColor : theme . bgPrimary ,
124148 borderRadius : 12 ,
149+ flexShrink : 1 ,
125150 } ,
151+ scroll : { padding : 12 } ,
126152 title : {
127153 padding : 12 ,
128154 fontSize : 18 ,
129155 fontWeight : '600' ,
130156 color : theme . textPrimary ,
131157 } ,
132- scroll : { padding : 12 } ,
133- field : { marginBottom : 12 } ,
134- label : { fontSize : 14 , marginBottom : 4 , color : theme . label } ,
135158 input : {
136159 borderWidth : 1 ,
137160 borderRadius : 8 ,
@@ -142,6 +165,16 @@ const getThemedStyles = (theme: any) =>
142165 backgroundColor : theme . inputBg ,
143166 fontFamily : 'monospace' ,
144167 } ,
168+ multiline : { minHeight : 250 , textAlignVertical : 'top' } ,
169+ errorText : {
170+ marginTop : 6 ,
171+ color : theme . errorBorder ,
172+ fontSize : 12 ,
173+ fontFamily : 'monospace' ,
174+ } ,
175+ error : {
176+ borderColor : theme . errorBorder ,
177+ } ,
145178 actions : {
146179 flexDirection : 'row' ,
147180 justifyContent : 'flex-end' ,
0 commit comments