moved code to mui-grid-form
Massimo Melina committed
May 31, 2022 at 18:28 UTC
1ba0f55731f28d1fc16e36bf011b8dd0db3b61d7
24 files changed
+323
-235
.gitignore
+2
-1
@@ -1,10 +1,11 @@
1
node_modules
2
.idea
3
dist
4
+shared/lib
5
+mui-grid-form/lib
6
#produced by running
7
counters.yaml
8
*.log*
9
self.*
10
config.yaml
11
accounts.yaml
10
-shared/lib
admin/src/AccountsPage.ts
+1
-1
@@ -4,7 +4,7 @@ import { isValidElement, createElement as h, useState, useEffect, Fragment, useR
4
import { apiCall, useApiComp } from './api'
5
import { Box, Button, Card, CardContent, Grid, List, ListItem, ListItemText, Typography } from '@mui/material'
6
import { Delete, Group, MilitaryTech, Person, PersonAdd, Refresh } from '@mui/icons-material'
7
-import { BoolField, Form, MultiSelectField } from './Form'
7
+import { BoolField, Form, MultiSelectField } from '@hfs/mui-grid-form'
8
import { alertDialog, confirmDialog } from './dialog'
9
import { iconTooltip, isEqualLax, modifiedSx, onlyTruthy } from './misc'
10
import { TreeItem, TreeView } from '@mui/lab'
admin/src/ArrayField.ts
+1
-1
@@ -3,7 +3,7 @@ import { IconBtn, setHidden } from './misc'
3
import { Add, Edit, Delete } from '@mui/icons-material'
4
import { confirmDialog, formDialog } from './dialog'
5
import { DataGrid, GridAlignment } from '@mui/x-data-grid'
6
-import { FieldDescriptor, FieldProps, labelFromKey } from './Form'
6
+import { FieldDescriptor, FieldProps, labelFromKey } from '@hfs/mui-grid-form'
7
import { Box, FormHelperText, FormLabel } from '@mui/material'
8
9
export function ArrayField<T=any>({ label, helperText, fields, value, onChange, onError, ...rest }: FieldProps<T[]> & { fields: FieldDescriptor[], height?: number }) {
admin/src/ConfigPage.ts
+1
-2
@@ -7,8 +7,7 @@ import { state, useSnapState } from './state'
7
import { Info, Refresh } from '@mui/icons-material'
8
import { Dict, modifiedSx } from './misc'
9
import { subscribeKey } from 'valtio/utils'
10
-import { Form, BoolField, NumberField, SelectField, FieldProps, Field } from './Form';
11
-import StringStringField from './StringStringField'
10
+import { Form, BoolField, NumberField, SelectField, StringStringField, FieldProps, Field } from '@hfs/mui-grid-form';
11
import FileField from './FileField'
12
import { alertDialog, closeDialog, confirmDialog, formDialog, newDialog, toast, waitDialog } from './dialog'
13
import { proxyWarning } from './HomePage'
admin/src/FileField.ts
+2
-2
@@ -1,9 +1,9 @@
1
-import { FieldProps, StringField } from './Form'
1
+import { FieldProps, StringField } from '@hfs/mui-grid-form'
2
import { createElement as h } from 'react'
3
import { InputAdornment } from '@mui/material'
4
import { Eject } from '@mui/icons-material'
5
import { IconBtn } from './misc'
6
-import { newDialog } from '@hfs/shared/lib/dialogs'
6
+import { newDialog } from '@hfs/shared'
7
import FilePicker from './FilePicker'
8
import { apiCall } from './api'
9
admin/src/FileForm.ts
+1
-1
@@ -3,7 +3,7 @@
3
import { state } from './state'
4
import { createElement as h, isValidElement, useEffect, useMemo, useState } from 'react'
5
import { Alert, Button } from '@mui/material'
6
-import { BoolField, DisplayField, Field, FieldProps, Form, MultiSelectField, SelectField } from './Form'
6
+import { BoolField, DisplayField, Field, FieldProps, Form, MultiSelectField, SelectField } from '@hfs/mui-grid-form'
7
import { apiCall, useApiComp } from './api'
8
import { formatBytes, isEqualLax, modifiedSx, onlyTruthy } from './misc'
9
import { reloadVfs, VfsNode, Who } from './VfsPage'
admin/src/FilePicker.ts
+1
-1
@@ -16,7 +16,7 @@ import {
16
} from '@mui/material'
17
import { enforceFinal, formatBytes, isWindowsDrive, spinner, Center } from './misc'
18
import { ArrowUpward, Home } from '@mui/icons-material'
19
-import { StringField } from './Form'
19
+import { StringField } from '@hfs/mui-grid-form'
20
import { FileIcon, FolderIcon } from './VfsTree'
21
import { FixedSizeList } from 'react-window'
22
import AutoSizer from "react-virtualized-auto-sizer"
admin/src/InstalledPlugins.ts
+1
-1
@@ -6,7 +6,7 @@ import { Delete, Error, GitHub, PlayCircle, Settings, StopCircle, SystemUpdateAl
6
import { IconBtn, xlate } from './misc'
7
import { formDialog, toast } from './dialog'
8
import _ from 'lodash'
9
-import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from './Form'
9
+import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from '@hfs/mui-grid-form'
10
import { ArrayField } from './ArrayField'
11
12
export default function InstalledPlugins({ updates }: { updates?: true }) {
admin/src/LoginRequired.ts
+1
-1
@@ -1,7 +1,7 @@
1
import { state, useSnapState } from './state'
2
import { createElement as h, Fragment, useState } from 'react'
3
import { Center } from './misc'
4
-import { Form } from './Form'
4
+import { Form } from '@hfs/mui-grid-form'
5
import { apiCall } from './api'
6
import { srpSequence } from '@hfs/shared'
7
import { Alert } from '@mui/material'
admin/src/MonitorPage.ts
+1
-1
@@ -8,7 +8,7 @@ import { Box, Chip } from '@mui/material'
8
import { DataGrid } from "@mui/x-data-grid"
9
import { Alert } from '@mui/material'
10
import { formatBytes, IconBtn, iconTooltip, manipulateConfig, useBreakpoint } from "./misc"
11
-import { Field, SelectField } from './Form'
11
+import { Field, SelectField } from '@hfs/mui-grid-form'
12
import { GridColumns } from '@mui/x-data-grid/models/colDef/gridColDef'
13
14
export default function MonitorPage() {
admin/src/OnlinePlugins.ts
+1
-1
@@ -4,7 +4,7 @@ import { DataGrid } from '@mui/x-data-grid'
4
import { IconBtn } from './misc'
5
import { Download, Search } from '@mui/icons-material'
6
import { toast } from './dialog'
7
-import { StringField } from './Form'
7
+import { StringField } from '@hfs/mui-grid-form'
8
import { useDebounce } from 'use-debounce'
9
import { repoLink, showError } from './InstalledPlugins'
10
admin/src/PermField.ts
+1
-1
@@ -3,7 +3,7 @@
3
import { Dict, useStateMounted } from './misc'
4
import { createElement as h, Fragment, isValidElement } from 'react'
5
import { Button, Grid } from '@mui/material'
6
-import { Field, FieldProps, SelectField } from './Form'
6
+import { Field, FieldProps, SelectField } from '@hfs/mui-grid-form'
7
import _ from 'lodash'
8
import { useApiComp } from './api'
9
admin/src/dialog.ts
+2
-2
@@ -19,8 +19,8 @@ import {
19
useState
20
} from 'react'
21
import { Check, Close, Error as ErrorIcon, Forward, Info, Warning } from '@mui/icons-material'
22
-import { newDialog, closeDialog, dialogsDefaults, DialogOptions } from '@hfs/shared/lib/dialogs'
23
-import { Form, FormProps } from './Form'
22
+import { newDialog, closeDialog, dialogsDefaults, DialogOptions } from '@hfs/shared'
23
+import { Form, FormProps } from '@hfs/mui-grid-form'
24
export * from '@hfs/shared/lib/dialogs'
25
26
dialogsDefaults.Container = function Container(d:DialogOptions) {
mui-grid-form/package.json
new
+19
@@ -0,0 +1,19 @@
1
+{
2
+ "name": "@hfs/mui-grid-form",
3
+ "main": "./lib/Form.js",
4
+ "types": "./lib/Form.d.ts",
5
+ "scripts": {
6
+ "build": "tsc"
7
+ },
8
+ "dependencies": {
9
+ "@mui/icons-material": "*",
10
+ "@mui/lab": "*",
11
+ "@mui/material": "*"
12
+ },
13
+ "devDependencies": {
14
+ "react": "*",
15
+ "react-dom": "*",
16
+ "react-scripts": "*",
17
+ "typescript": "*"
18
+ }
19
+}
mui-grid-form/src/Form.ts
renamed
+6
-212
@@ -12,22 +12,15 @@ import {
12
useRef,
13
MutableRefObject
14
} from 'react'
15
-import {
16
- Box, BoxProps, Button,
17
- Checkbox,
18
- FormControl,
19
- FormControlLabel, FormGroup, FormHelperText,
20
- FormLabel,
21
- Grid, InputAdornment,
22
- MenuItem, Radio,
23
- RadioGroup,
24
- Switch,
25
- TextField
26
-} from '@mui/material'
15
+import { Box, BoxProps, Button, Grid } from '@mui/material'
16
import { Save } from '@mui/icons-material'
17
import { LoadingButton } from '@mui/lab'
18
import _ from 'lodash'
30
-import { SxProps } from '@mui/system'
19
+import { StringField } from './StringField'
20
+export * from './SelectField'
21
+export * from './misc-fields'
22
+export * from './StringStringField'
23
+export { StringField }
24
25
type ValidationError = string | boolean // false = no error
26
export interface FieldDescriptor<T=any> {
@@ -223,202 +216,3 @@ export interface FieldProps<T> {
216
error?: true
217
[rest: string]: any
218
}
226
-
227
-export function StringField({ value, onChange, min, max, required, getApi, typing, start, end, ...props }: FieldProps<string>) {
228
- const setter = () => value ?? ''
229
- getApi?.({
230
- getError() {
231
- return !value && required ? "required"
232
- : value?.length! < min ? "too short"
233
- : value?.length! > max ? "too long"
234
- : false
235
- }
236
- })
237
- const [state, setState] = useState(setter)
238
-
239
- const lastChange = useRef(value)
240
- useEffect(() => setState(setter), [value]) //eslint-disable-line
241
- return h(TextField, {
242
- fullWidth: true,
243
- InputLabelProps: state || props.placeholder ? { shrink: true } : undefined,
244
- ...props,
245
- value: state,
246
- onChange(ev) {
247
- const val = ev.target.value
248
- setState(val)
249
- if (typing // change state as the user is typing
250
- || document.activeElement !== ev.target) // autofill ongoing, don't wait onBlur event, just go
251
- go(ev, val)
252
- },
253
- onKeyDown(ev) {
254
- props.onKeyDown?.(ev)
255
- if (ev.key === 'Enter')
256
- go(ev)
257
- },
258
- onBlur(ev) {
259
- props.onBlur?.(ev)
260
- go(ev)
261
- },
262
- InputProps: {
263
- startAdornment: start && h(InputAdornment, { position: 'start' }, start),
264
- endAdornment: end && h(InputAdornment, { position: 'end' }, end),
265
- ...props.InputProps,
266
- },
267
- })
268
-
269
- function go(event: any, val: string=state) {
270
- const newV = val.trim()
271
- if (newV === lastChange.current) return // don't compare to 'value' as that represents only accepted changes, while we are interested also in changes through discarded values
272
- lastChange.current = newV
273
- onChange(newV, {
274
- was: value,
275
- event,
276
- cancel() {
277
- setState(setter)
278
- }
279
- })
280
- }
281
-}
282
-
283
-export function DisplayField({ value, empty='-', ...props }: any) {
284
- if (!props.toField && empty !== undefined && value !== 0 && !value)
285
- value = empty
286
- return h(StringField, { ...props, value, disabled: true })
287
-}
288
-
289
-type SelectOptions<T> = { [label:string]:T } | SelectOption<T>[]
290
-type SelectOption<T> = SelectPair<T> | (T extends string | number ? T : never)
291
-interface SelectPair<T> { label: string, value:T }
292
-
293
-export function SelectField<T>(props: FieldProps<T> & { options:SelectOptions<T> }) {
294
- const { value, onChange, getApi, options, sx, ...rest } = props
295
- return h(TextField, { // using TextField because Select is not displaying label correctly
296
- ...commonSelectProps(props),
297
- ...rest,
298
- onChange(event) {
299
- try {
300
- let newVal: any = event.target.value
301
- newVal = JSON.parse(newVal) as T
302
- onChange(newVal, { was: value, event })
303
- }
304
- catch {}
305
- }
306
- })
307
-}
308
-
309
-export function MultiSelectField<T>(props: FieldProps<T[]> & { options:SelectOptions<T> }) {
310
- const { value, onChange, getApi, options, sx, ...rest } = props
311
- return h(TextField, {
312
- ...commonSelectProps({ ...props, value: undefined }),
313
- ...rest,
314
- SelectProps: { multiple: true },
315
- value: !Array.isArray(value) ? [] : value.map(x => JSON.stringify(x)),
316
- onChange(event) {
317
- try {
318
- let v: any = event.target.value
319
- v = Array.isArray(v) ? v.map(x => JSON.parse(x)) : []
320
- onChange(v as T[], { was: value, event })
321
- }
322
- catch {}
323
- }
324
- })
325
-}
326
-
327
-function commonSelectProps<T>(props: { sx?:SxProps, label?: FieldProps<T>['label'], value?: T, disabled?: boolean, options:SelectOptions<T> }) {
328
- const { options, disabled } = props
329
- const normalizedOptions = !Array.isArray(options) ? Object.entries(options).map(([label,value]) => ({ value, label }))
330
- : options.map(o => typeof o === 'string' || typeof o === 'number' ? { value: o, label: String(o) } : o as SelectPair<T>)
331
- const jsonValue = JSON.stringify(props.value)
332
- const currentOption = normalizedOptions.find(x => JSON.stringify(x.value) === jsonValue)
333
- return {
334
- select: true,
335
- fullWidth: true,
336
- sx: props.label ? props.sx : Object.assign({ '& .MuiInputBase-input': { pt: 1 } }, props.sx),
337
- // avoid warning for invalid option. This can easily happen for a split-second when you keep value in a useState (or other async way) and calculate options with a useMemo (or other sync way) causing a temporary misalignment.
338
- value: currentOption ? jsonValue : '',
339
- disabled: !normalizedOptions?.length || disabled,
340
- children: normalizedOptions.map((o, i) => h(MenuItem, {
341
- key: i,
342
- value: JSON.stringify(o?.value),
343
- children: o?.label
344
- }))
345
- }
346
-}
347
-
348
-export function NumberField({ value, onChange, getApi, required, min, max, step, ...props }: FieldProps<number | null>) {
349
- getApi?.({
350
- getError() {
351
- return value == null ? (required ? "required" : false)
352
- : (value < min ? "too low" : value > max ? "too high" : false)
353
- }
354
- })
355
- return h(StringField, {
356
- type: 'number',
357
- value: value == null ? '' : String(value),
358
- onChange(v, { was, ...rest }) {
359
- onChange(!v ? null : Number(v), {
360
- ...rest,
361
- was: was ? Number(was) : null,
362
- })
363
- },
364
- inputProps: { min, max, step, },
365
- ...props,
366
- })
367
-}
368
-
369
-export function BoolField({ label='', value, onChange, getApi, helperText, error,
370
- type, // avoid passing this by accident, as it disrupts the control
371
- ...props }: FieldProps<boolean>) {
372
- const setter = () => value ?? false
373
- const [state, setState] = useState(setter)
374
- useEffect(() => setState(setter),
375
- [value]) //eslint-disable-line
376
- const control = h(Switch, {
377
- checked: state,
378
- ...props,
379
- onChange(event) {
380
- onChange(event.target.checked, { event, was: value })
381
- }
382
- })
383
- return h(Box, { ml: 1, mt: 1, sx: error && { color: 'error.main', outlineOffset: 6, outline: '1px solid' } },
384
- h(FormControlLabel, { label, control, labelPlacement: 'end' }),
385
- helperText && h(FormHelperText, { error }, helperText)
386
- )
387
-}
388
-
389
-export function RadioField<T>({ label, options, value, onChange }: FieldProps<T> & { options:SelectPair<T>[] }) {
390
- return h(FormControl, {},
391
- label && h(FormLabel, {}, label),
392
- h(RadioGroup, {
393
- row: true,
394
- name: '',
395
- value: JSON.stringify(value),
396
- onChange(event, v){
397
- onChange(JSON.parse(v), { was: value, event })
398
- }
399
- },
400
- options.map(({ value, label }, idx) =>
401
- h(FormControlLabel, { key: idx, value, control: h(Radio), label }) )
402
- )
403
- )
404
-}
405
-
406
-export function CheckboxesField({ label, options, value, onChange }: FieldProps<string[]> & { options: string[] }) {
407
- return h(FormControl, {},
408
- label && h(FormLabel, {}, label),
409
- h(FormGroup, {},
410
- options.map(o => {
411
- const checked = value?.includes(o)
412
- return h(FormControlLabel, {
413
- key: o,
414
- checked,
415
- control: h(Checkbox),
416
- label: o,
417
- onClick(event) {
418
- const newValue = checked ? value!.filter(x => x !== o) : [...value||[], o]
419
- onChange(newValue, { was: value, event })
420
- }
421
- })
422
- }))
423
- )
424
-}
mui-grid-form/src/SelectField.ts
new
+81
@@ -0,0 +1,81 @@
1
+import { createElement as h } from 'react'
2
+import { FieldProps } from './Form'
3
+import { FormControl, FormControlLabel, FormLabel, MenuItem, Radio, RadioGroup, TextField } from '@mui/material'
4
+import { SxProps } from '@mui/system'
5
+
6
+type SelectOptions<T> = { [label:string]:T } | SelectOption<T>[]
7
+type SelectOption<T> = SelectPair<T> | (T extends string | number ? T : never)
8
+interface SelectPair<T> { label: string, value:T }
9
+
10
+export function SelectField<T>(props: FieldProps<T> & { options:SelectOptions<T> }) {
11
+ const { value, onChange, getApi, options, sx, ...rest } = props
12
+ return h(TextField, { // using TextField because Select is not displaying label correctly
13
+ ...commonSelectProps(props),
14
+ ...rest,
15
+ onChange(event) {
16
+ try {
17
+ let newVal: any = event.target.value
18
+ newVal = JSON.parse(newVal) as T
19
+ onChange(newVal, { was: value, event })
20
+ }
21
+ catch {}
22
+ }
23
+ })
24
+}
25
+
26
+export function MultiSelectField<T>(props: FieldProps<T[]> & { options:SelectOptions<T> }) {
27
+ const { value, onChange, getApi, options, sx, ...rest } = props
28
+ return h(TextField, {
29
+ ...commonSelectProps({ ...props, value: undefined }),
30
+ ...rest,
31
+ SelectProps: { multiple: true },
32
+ value: !Array.isArray(value) ? [] : value.map(x => JSON.stringify(x)),
33
+ onChange(event) {
34
+ try {
35
+ let v: any = event.target.value
36
+ v = Array.isArray(v) ? v.map(x => JSON.parse(x)) : []
37
+ onChange(v as T[], { was: value, event })
38
+ }
39
+ catch {}
40
+ }
41
+ })
42
+}
43
+
44
+function commonSelectProps<T>(props: { sx?:SxProps, label?: FieldProps<T>['label'], value?: T, disabled?: boolean, options:SelectOptions<T> }) {
45
+ const { options, disabled } = props
46
+ const normalizedOptions = !Array.isArray(options) ? Object.entries(options).map(([label,value]) => ({ value, label }))
47
+ : options.map(o => typeof o === 'string' || typeof o === 'number' ? { value: o, label: String(o) } : o as SelectPair<T>)
48
+ const jsonValue = JSON.stringify(props.value)
49
+ const currentOption = normalizedOptions.find(x => JSON.stringify(x.value) === jsonValue)
50
+ return {
51
+ select: true,
52
+ fullWidth: true,
53
+ sx: props.label ? props.sx : Object.assign({ '& .MuiInputBase-input': { pt: 1 } }, props.sx),
54
+ // avoid warning for invalid option. This can easily happen for a split-second when you keep value in a useState (or other async way) and calculate options with a useMemo (or other sync way) causing a temporary misalignment.
55
+ value: currentOption ? jsonValue : '',
56
+ disabled: !normalizedOptions?.length || disabled,
57
+ children: normalizedOptions.map((o, i) => h(MenuItem, {
58
+ key: i,
59
+ value: JSON.stringify(o?.value),
60
+ children: o?.label
61
+ }))
62
+ }
63
+}
64
+
65
+export function RadioField<T>({ label, options, value, onChange }: FieldProps<T> & { options:SelectPair<T>[] }) {
66
+ return h(FormControl, {},
67
+ label && h(FormLabel, {}, label),
68
+ h(RadioGroup, {
69
+ row: true,
70
+ name: '',
71
+ value: JSON.stringify(value),
72
+ onChange(event, v){
73
+ onChange(JSON.parse(v), { was: value, event })
74
+ }
75
+ },
76
+ options.map(({ value, label }, idx) =>
77
+ h(FormControlLabel, { key: idx, value, control: h(Radio), label }) )
78
+ )
79
+ )
80
+}
81
+
mui-grid-form/src/StringField.ts
new
+60
@@ -0,0 +1,60 @@
1
+import { createElement as h, useEffect, useRef, useState } from 'react'
2
+import { FieldProps } from './Form'
3
+import { InputAdornment, TextField } from '@mui/material'
4
+
5
+export function StringField({ value, onChange, min, max, required, getApi, typing, start, end, ...props }: FieldProps<string>) {
6
+ const setter = () => value ?? ''
7
+ getApi?.({
8
+ getError() {
9
+ return !value && required ? "required"
10
+ : value?.length! < min ? "too short"
11
+ : value?.length! > max ? "too long"
12
+ : false
13
+ }
14
+ })
15
+ const [state, setState] = useState(setter)
16
+
17
+ const lastChange = useRef(value)
18
+ useEffect(() => setState(setter), [value]) //eslint-disable-line
19
+ return h(TextField, {
20
+ fullWidth: true,
21
+ InputLabelProps: state || props.placeholder ? { shrink: true } : undefined,
22
+ ...props,
23
+ value: state,
24
+ onChange(ev) {
25
+ const val = ev.target.value
26
+ setState(val)
27
+ if (typing // change state as the user is typing
28
+ || document.activeElement !== ev.target) // autofill ongoing, don't wait onBlur event, just go
29
+ go(ev, val)
30
+ },
31
+ onKeyDown(ev) {
32
+ props.onKeyDown?.(ev)
33
+ if (ev.key === 'Enter')
34
+ go(ev)
35
+ },
36
+ onBlur(ev) {
37
+ props.onBlur?.(ev)
38
+ go(ev)
39
+ },
40
+ InputProps: {
41
+ startAdornment: start && h(InputAdornment, { position: 'start' }, start),
42
+ endAdornment: end && h(InputAdornment, { position: 'end' }, end),
43
+ ...props.InputProps,
44
+ },
45
+ })
46
+
47
+ function go(event: any, val: string=state) {
48
+ const newV = val.trim()
49
+ if (newV === lastChange.current) return // don't compare to 'value' as that represents only accepted changes, while we are interested also in changes through discarded values
50
+ lastChange.current = newV
51
+ onChange(newV, {
52
+ was: value,
53
+ event,
54
+ cancel() {
55
+ setState(setter)
56
+ }
57
+ })
58
+ }
59
+}
60
+
mui-grid-form/src/StringStringField.ts
renamed
+1
-2
@@ -1,7 +1,6 @@
1
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { makeStyles } from '@mui/styles'
4
-import { Dict } from './misc'
4
import { createElement as h, useRef } from 'react'
5
import { Grid, IconButton } from '@mui/material'
6
import { Add, Delete } from '@mui/icons-material'
@@ -17,7 +16,7 @@ const useStyles = makeStyles({
16
}
17
})
18
20
-export default function StringStringField({ value, onChange, keyLabel='key', valueLabel='value', keyWidth=5, valueWidth=5, actionsWidth=1 }: FieldProps<Dict<string>> & { keyLabel:string }) {
19
+export function StringStringField({ value, onChange, keyLabel='key', valueLabel='value', keyWidth=5, valueWidth=5, actionsWidth=1 }: FieldProps<Record<string,string>> & { keyLabel:string }) {
20
const refNew = useRef()
21
const justEntered = useRef<any>()
22
const styles = useStyles()
mui-grid-form/src/misc-fields.ts
new
+80
@@ -0,0 +1,80 @@
1
+import { createElement as h, useEffect, useState } from 'react'
2
+import { StringField } from './StringField'
3
+import { FieldProps } from './Form'
4
+import {
5
+ Box,
6
+ Checkbox,
7
+ FormControl,
8
+ FormControlLabel,
9
+ FormGroup,
10
+ FormHelperText,
11
+ FormLabel,
12
+ Switch
13
+} from '@mui/material'
14
+
15
+export function DisplayField({ value, empty='-', ...props }: any) {
16
+ if (!props.toField && empty !== undefined && value !== 0 && !value)
17
+ value = empty
18
+ return h(StringField, { ...props, value, disabled: true })
19
+}
20
+
21
+export function NumberField({ value, onChange, getApi, required, min, max, step, ...props }: FieldProps<number | null>) {
22
+ getApi?.({
23
+ getError() {
24
+ return value == null ? (required ? "required" : false)
25
+ : (value < min ? "too low" : value > max ? "too high" : false)
26
+ }
27
+ })
28
+ return h(StringField, {
29
+ type: 'number',
30
+ value: value == null ? '' : String(value),
31
+ onChange(v, { was, ...rest }) {
32
+ onChange(!v ? null : Number(v), {
33
+ ...rest,
34
+ was: was ? Number(was) : null,
35
+ })
36
+ },
37
+ inputProps: { min, max, step, },
38
+ ...props,
39
+ })
40
+}
41
+
42
+export function BoolField({ label='', value, onChange, getApi, helperText, error,
43
+ type, // avoid passing this by accident, as it disrupts the control
44
+ ...props }: FieldProps<boolean>) {
45
+ const setter = () => value ?? false
46
+ const [state, setState] = useState(setter)
47
+ useEffect(() => setState(setter),
48
+ [value]) //eslint-disable-line
49
+ const control = h(Switch, {
50
+ checked: state,
51
+ ...props,
52
+ onChange(event) {
53
+ onChange(event.target.checked, { event, was: value })
54
+ }
55
+ })
56
+ return h(Box, { ml: 1, mt: 1, sx: error && { color: 'error.main', outlineOffset: 6, outline: '1px solid' } },
57
+ h(FormControlLabel, { label, control, labelPlacement: 'end' }),
58
+ helperText && h(FormHelperText, { error }, helperText)
59
+ )
60
+}
61
+
62
+export function CheckboxesField({ label, options, value, onChange }: FieldProps<string[]> & { options: string[] }) {
63
+ return h(FormControl, {},
64
+ label && h(FormLabel, {}, label),
65
+ h(FormGroup, {},
66
+ options.map(o => {
67
+ const checked = value?.includes(o)
68
+ return h(FormControlLabel, {
69
+ key: o,
70
+ checked,
71
+ control: h(Checkbox),
72
+ label: o,
73
+ onClick(event) {
74
+ const newValue = checked ? value!.filter(x => x !== o) : [...value||[], o]
75
+ onChange(newValue, { was: value, event })
76
+ }
77
+ })
78
+ }))
79
+ )
80
+}
mui-grid-form/tsconfig.json
new
+30
@@ -0,0 +1,30 @@
1
+{
2
+ "compilerOptions": {
3
+ "target": "es2017",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "strict": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "node",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "outDir": "lib",
19
+ "declaration": true
20
+ },
21
+ "exclude": [
22
+ "node_modules",
23
+ "lib-esm",
24
+ "lib",
25
+ "**/*.spec*"
26
+ ],
27
+ "include": [
28
+ "src"
29
+ ]
30
+}
package-lock.json
+24
-1
@@ -12,7 +12,8 @@
12
"server",
13
"admin",
14
"frontend",
15
- "shared"
15
+ "shared",
16
+ "mui-grid-form"
17
],
18
"bin": {
19
"hfs": "dist/src/index.js"
@@ -87,6 +88,15 @@
88
"sass": "^1.45.0"
89
}
90
},
91
+ "mui-grid-form": {
92
+ "name": "@hfs/mui-grid-form",
93
+ "devDependencies": {
94
+ "react": "*",
95
+ "react-dom": "*",
96
+ "react-scripts": "*",
97
+ "typescript": "*"
98
+ }
99
+ },
100
"node_modules/@ampproject/remapping": {
101
"version": "2.2.0",
102
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
@@ -2756,6 +2766,10 @@
2766
"resolved": "frontend",
2767
"link": true
2768
},
2769
+ "node_modules/@hfs/mui-grid-form": {
2770
+ "resolved": "mui-grid-form",
2771
+ "link": true
2772
+ },
2773
"node_modules/@hfs/server": {
2774
"resolved": "server",
2775
"link": true
@@ -22859,6 +22873,15 @@
22873
"web-vitals": "^2.1.4"
22874
}
22875
},
22876
+ "@hfs/mui-grid-form": {
22877
+ "version": "file:mui-grid-form",
22878
+ "requires": {
22879
+ "react": "*",
22880
+ "react-dom": "*",
22881
+ "react-scripts": "*",
22882
+ "typescript": "*"
22883
+ }
22884
+ },
22885
"@hfs/server": {
22886
"version": "file:server",
22887
"requires": {
package.json
+4
-3
@@ -9,17 +9,18 @@
9
"homepage": "https://rejetto.com/hfs",
10
"license": "GPL-3.0",
11
"author": "Massimo Melina <a@rejetto.com>",
12
- "workspaces": [ "server", "admin", "frontend", "shared" ],
12
+ "workspaces": [ "server", "admin", "frontend", "shared", "mui-grid-form" ],
13
"scripts": {
14
"watch-server": "nodemon --ignore tests/ --watch server/src -e ts,tsx --exec ts-node server/src/index.ts",
15
"watch-shared": "cd shared && tsc --watch",
16
"start-frontend": "cd frontend && npm run start",
17
"start-admin": "cd admin && npm run start",
18
- "build-all": "npm audit --omit=dev && rm -rf dist && npm install && npm run build-shared && npm run build-server && npm run build-frontend && npm run build-admin && echo COMPLETED",
18
+ "build-all": "npm audit --omit=dev && rm -rf dist && npm install && npm run build-shared && npm run build-mui-grid-form && npm run build-server && npm run build-frontend && npm run build-admin && echo COMPLETED",
19
"build-server": "rm -rf dist/src dist/plugins && tsc --target es2018 && cp -v -r package-lock.json server/package.json READ* LICENSE* plugins dist && cd dist && npm ci --production && cd .. && node afterbuild",
20
"build-frontend": "cd frontend && npm run build",
21
"build-admin": "cd admin && npm run build",
22
- "build-shared": "cd shared && npx tsc",
22
+ "build-shared": "cd shared && npm run build",
23
+ "build-mui-grid-form": "cd mui-grid-form && npm run build",
24
"server-for-test": "cross-env HFS_CONFIG=tests node dist/src",
25
"test": "mocha -r ts-node/register 'tests/**/*.ts'",
26
"dist-node": "cd dist && zip hfs-node.zip -r * -x *.zip *.exe hfs-*",
shared/src/index.ts
+1
@@ -3,6 +3,7 @@
3
import _ from 'lodash'
4
import { SRPClientSession, SRPParameters, SRPRoutines } from 'tssrp6a'
5
export * from './react'
6
+export * from './dialogs'
7
8
export type Dict<T=any> = Record<string, T>
9
export type Falsy = false | null | undefined | '' | 0
tsconfig.json
+1
-1
@@ -1,5 +1,5 @@
1
{
2
- "exclude": ["frontend","admin","tests","dist","shared"],
2
+ "exclude": ["frontend","admin","tests","dist","shared","mui-grid-form"],
3
"compilerOptions": {
4
/* Visit https://aka.ms/tsconfig.json to read more about this file */
5