admin: from/toField moved to Form, while fields can now generate errors without form logic
Massimo Melina committed
May 3, 2022 at 17:33 UTC
da2fade8bd1bfbe51e6a1aa98bf1eccd6c1db607
7 files changed
+59
-57
admin/src/ConfigPage.ts
-1
@@ -44,7 +44,6 @@ export default function ConfigPage() {
44
comp: NumberField,
45
min: 1,
46
placeholder: "no limit",
47
- onChange: (v: any) => v < 1 ? null : v,
47
}
48
return h(Form, {
49
sx: { maxWidth: '60em' },
admin/src/FileForm.ts
+1
-1
@@ -95,7 +95,7 @@ export default function FileForm({ file }: { file: VfsNode }) {
95
toField: Boolean, fromField: (v:boolean) => v ? 'index.html' : null,
96
helperText: md("This folder may be a website because contains `index.html`. Enabling this will show the website instead of the list of files.")
97
},
98
- isDir && { k: 'masks', multiline: true, xl: 6, toField: JSON.stringify, fromField: JSON.parse,
98
+ isDir && { k: 'masks', multiline: true, xl: 6, toField: JSON.stringify, fromField: v => v ? JSON.parse(v) : undefined,
99
helperText: "This is a special field. Leave it empty unless you know what you are doing." }
100
]
101
})
admin/src/FilePicker.ts
+1
-1
@@ -76,7 +76,7 @@ export default function FilePicker({ onSelect, multiple=true, from }: FilePicker
76
h(StringField, {
77
label: 'Current path',
78
value: cwd,
79
- onChange: setCwd
79
+ onChange: setCwd as any,
80
}),
81
),
82
error ? h(Alert, { severity:'error' }, String(error))
admin/src/Form.ts
+49
-51
@@ -33,7 +33,8 @@ export interface FieldDescriptor<T=any> {
33
comp?: any
34
label?: ReactNode
35
validate?: RegExp | ((v: any, extra:any) => string | boolean)
36
- onChange?: (v: T) => void
36
+ toField?: (v: T) => any
37
+ fromField?: (v: any) => T
38
[extraProp: string]: any
39
}
40
@@ -65,6 +66,7 @@ export function Form<Values extends Dict>({ fields, values, set, defaults, save,
66
67
const [loading, setLoading] = useState(false)
68
const [errors, setErrors] = useState<Dict>({})
69
+ const [fieldErrors, setFieldErrors] = useState<Dict>({})
70
const saveBtn = typeof save === 'function' ? { onClick: save } : save // normalize
71
const [pendingSubmit, setPendingSubmit] = useState(false)
72
useEffect(() => {
@@ -96,19 +98,23 @@ export function Form<Values extends Dict>({ fields, values, set, defaults, save,
98
if (isValidElement(row))
99
return h(Grid, { key: idx, item: true, xs: 12 }, row)
100
let field = row
99
- const { k, onChange } = field
100
- let error = errors[k]
101
+ const { k, fromField=_.identity, toField=_.identity } = field
102
+ let error = errors[k] || fieldErrors[k]
103
if (error === true)
104
error = "Not valid"
105
if (k) {
106
+ const originalValue = values?.[k]
107
field = {
105
- value: values?.[k],
108
+ value: toField(originalValue),
109
...field,
110
error: field.error || Boolean(error) || undefined,
108
- onChange(v:any) {
109
- if (onChange)
110
- v = onChange(v)
111
- set(v, field.k)
111
+ onChange(v: any) {
112
+ try { v = fromField(v) } // catch parsing exceptions
113
+ catch (e) { v = e }
114
+ const err = v instanceof Error ? v.message || true : undefined
115
+ setFieldErrors({ ...fieldErrors, [k]: err })
116
+ if (!err && !_.isEqual(v, originalValue))
117
+ set(v, k)
118
},
119
}
120
if (error)
@@ -118,11 +124,13 @@ export function Form<Values extends Dict>({ fields, values, set, defaults, save,
124
field.label = labelFromKey(k)
125
_.defaults(field, defaults?.(field))
126
}
121
- const { xs=12, sm, md, lg, xl, comp=StringField,
122
- validate, // don't propagate
123
- ...rest } = field
124
- return h(Grid, { key: k, item: true, xs, sm, md, lg, xl },
125
- isValidElement(comp) ? comp : h(comp, rest) )
127
+ {
128
+ const { xs=12, sm, md, lg, xl, comp=StringField,
129
+ validate, fromField, toField, // don't propagate
130
+ ...rest } = field
131
+ return h(Grid, { key: k, item: true, xs, sm, md, lg, xl },
132
+ isValidElement(comp) ? comp : h(comp, rest) )
133
+ }
134
})
135
),
136
saveBtn && h(Box, {
@@ -148,19 +156,25 @@ export function Form<Values extends Dict>({ fields, values, set, defaults, save,
156
async function wrappedSave(...args: Parameters<NonNullable<typeof saveBtn.onClick>>) {
157
const cb = saveBtn.onClick
158
if (!cb) return
159
+ const MSG = "Please review errors"
160
setLoading(true)
161
try {
162
for (const f of fields) {
154
- if (!f || isValidElement(f) || !f.k || !f.validate) continue
163
+ if (!f || isValidElement(f) || !f.k) continue
164
+ if (fieldErrors[f.k])
165
+ return onError?.(MSG)
166
let fv = f.validate
167
+ if (!fv) continue
168
if (fv instanceof RegExp) {
169
const re = fv
170
fv = x => re.test(x)
171
}
172
const res = await fv(values?.[f.k], { values, fields })
173
if (!mounted.current) return
162
- if (res !== true)
163
- return setErrors({ [f.k]: res || true })
174
+ if (res !== true) {
175
+ setErrors({ [f.k]: res || true })
176
+ return onError?.(MSG)
177
+ }
178
}
179
setErrors({})
180
return await cb(...args)
@@ -181,27 +195,18 @@ export function labelFromKey(k: string) {
195
export interface FieldProps<T> {
196
label?: string | ReactElement
197
value?: T
184
- onChange: (v: T, more: { was?: T, event: any, [rest: string]: any }) => void
185
- toField?: (v: any) => T
186
- fromField?: (v: T) => any
198
+ onChange: (v: T | Error, more: { was?: T, event: any, [rest: string]: any }) => void
199
error?: true
200
[rest: string]: any
201
}
202
191
-export function StringField({ value, onChange, fromField=_.identity, toField=_.identity, ...props }: FieldProps<string>) {
192
- if (fromField === JSON.parse)
193
- fromField = v => v ? JSON.parse(v) : undefined
194
- const [state, setState] = useState(() => toField(value) ?? '')
195
- const [err, setErr] = useState('')
196
- if (err) {
197
- props.error = true
198
- props.helperText = h(Fragment, {}, err, props.helperText && h('br'), props.helperText ) // keep existing helperText, if any
199
- }
203
+export function StringField({ value, onChange, ...props }: FieldProps<string>) {
204
+ const setter = () => value ?? ''
205
+ const [state, setState] = useState(setter)
206
207
useEffect(() => {
202
- setState(() => toField(value) ?? '')
203
- setErr('')
204
- }, [value, toField])
208
+ setState(setter)
209
+ }, [value])
210
return h(TextField, {
211
fullWidth: true,
212
InputLabelProps: state || props.placeholder ? { shrink: true } : undefined,
@@ -226,21 +231,13 @@ export function StringField({ value, onChange, fromField=_.identity, toField=_.i
231
})
232
233
function go(event: any, val: string=state) {
229
- let newV
230
- try { // catch parsing exceptions
231
- newV = fromField(val.trim())
232
- }
233
- catch (e) {
234
- return setErr(String(e))
235
- }
236
- if (newV !== value)
237
- onChange(newV, {
238
- was: value,
239
- event,
240
- cancel() {
241
- setState(value ?? '')
242
- }
243
- })
234
+ onChange(val.trim(), {
235
+ was: value,
236
+ event,
237
+ cancel() {
238
+ setState(setter)
239
+ }
240
+ })
241
}
242
}
243
@@ -310,22 +307,23 @@ function commonSelectProps<T>(props: { sx?:SxProps, label?: FieldProps<T>['label
307
}
308
309
export function NumberField({ value, onChange, min, max, step, ...props }: FieldProps<number | null>) {
313
- // @ts-ignore
310
return h(StringField, {
311
type: 'number',
312
value: typeof value === 'number' ? String(value) : '',
313
onChange(v, { was, ...rest }) {
318
- onChange(v ? Number(v) : null, { ...rest, was:was ? Number(was) : null })
314
+ const n = Number(v)
315
+ onChange(!v ? null : n < min ? Error('too low') : n > max ? Error('too high') : n,
316
+ { ...rest, was: was ? Number(was) : null })
317
},
318
inputProps: { min, max, step, },
319
...props,
320
})
321
}
322
325
-export function BoolField({ label='', value, onChange, helperText, error, fromField=_.identity, toField=_.identity,
323
+export function BoolField({ label='', value, onChange, helperText, error,
324
type, // avoid passing this by accident, as it disrupts the control
325
...props }: FieldProps<boolean>) {
328
- const setter = () => toField(value) ?? false
326
+ const setter = () => value ?? false
327
const [state, setState] = useState(setter)
328
useEffect(() => setState(setter),
329
[value]) //eslint-disable-line
@@ -333,7 +331,7 @@ export function BoolField({ label='', value, onChange, helperText, error, fromFi
331
checked: state,
332
...props,
333
onChange(event) {
336
- onChange(fromField(event.target.checked), { event, was: value })
334
+ onChange(event.target.checked, { event, was: value })
335
}
336
})
337
return h(Box, { ml: 1, mt: 1, sx: error && { color: 'error.main', outlineOffset: 6, outline: '1px solid' } },
admin/src/MonitorPage.ts
+1
-1
@@ -143,7 +143,7 @@ function Connections() {
143
h(SelectField as Field<boolean>, {
144
fullWidth: false,
145
value: filtered,
146
- onChange: setFiltered,
146
+ onChange: setFiltered as any,
147
options: { "Downloads connections": true, "All connections": false }
148
}),
149
admin/src/PermField.ts
+4
-1
@@ -33,6 +33,7 @@ export default function PermField({ label, value, onChange }: FieldProps<Dict<st
33
options: usernames,
34
value: username,
35
onChange(v, { was, ...rest }){
36
+ if (v instanceof Error) return
37
const copy: any = { ...value, [v]: value![was!] }
38
delete copy[was!]
39
onChange(copy, { was:value, ...rest })
@@ -44,6 +45,7 @@ export default function PermField({ label, value, onChange }: FieldProps<Dict<st
45
options: permOptions,
46
value: perm,
47
onChange(v, { was, ...rest }){
48
+ if (v instanceof Error) return
49
const copy = { ...value }
50
if (v)
51
copy[username] = v
@@ -60,7 +62,7 @@ export default function PermField({ label, value, onChange }: FieldProps<Dict<st
62
label: value ? "Add access to" : "Restrict access to ",
63
value: temp,
64
options: usernamesLeft,
63
- onChange: setTemp
65
+ onChange: setTemp as any,
66
})),
67
usernamesLeft.length>0 && h(Grid, { item: true, xs: 6 },
68
h(SelectField as Field<string>, {
@@ -69,6 +71,7 @@ export default function PermField({ label, value, onChange }: FieldProps<Dict<st
71
disabled: !temp,
72
options: permOptions,
73
onChange(v, rest) {
74
+ if (v instanceof Error) return
75
if (v)
76
onChange({ ...value, [temp!]: v }, { ...rest, was: value })
77
setTemp(undefined)
admin/src/StringStringField.ts
+3
-1
@@ -39,6 +39,7 @@ export default function StringStringField({ value, onChange, keyLabel='key', val
39
h(StringField, {
40
value: id,
41
onChange(v, { was, ...rest }){
42
+ if (v instanceof Error) return
43
const copy = { ...value }
44
if (v)
45
copy[v] = was !== undefined ? copy[was] : ''
@@ -52,6 +53,7 @@ export default function StringStringField({ value, onChange, keyLabel='key', val
53
inputRef: justEntered.current === id ? justEntered : undefined,
54
value: v,
55
onChange(v, { was, ...rest }){
56
+ if (v instanceof Error) return
57
const copy = { ...value }
58
if (v)
59
copy[id] = v
@@ -75,7 +77,7 @@ export default function StringStringField({ value, onChange, keyLabel='key', val
77
inputRef: refNew,
78
value: '',
79
onChange(v, more){
78
- if (!v) return
80
+ if (!v || v instanceof Error) return
81
more.cancel()
82
if (value && v in value)
83
return alert(keyLabel + " entry already present")