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