Skip to content

Commit

Permalink
moved code to mui-grid-form
Browse files Browse the repository at this point in the history
  • Loading branch information
rejetto committed May 31, 2022
1 parent f9a446a commit 1ba0f55
Show file tree
Hide file tree
Showing 24 changed files with 323 additions and 235 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
node_modules
.idea
dist
shared/lib
mui-grid-form/lib
#produced by running
counters.yaml
*.log*
self.*
config.yaml
accounts.yaml
shared/lib
2 changes: 1 addition & 1 deletion admin/src/AccountsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { isValidElement, createElement as h, useState, useEffect, Fragment, useR
import { apiCall, useApiComp } from './api'
import { Box, Button, Card, CardContent, Grid, List, ListItem, ListItemText, Typography } from '@mui/material'
import { Delete, Group, MilitaryTech, Person, PersonAdd, Refresh } from '@mui/icons-material'
import { BoolField, Form, MultiSelectField } from './Form'
import { BoolField, Form, MultiSelectField } from '@hfs/mui-grid-form'
import { alertDialog, confirmDialog } from './dialog'
import { iconTooltip, isEqualLax, modifiedSx, onlyTruthy } from './misc'
import { TreeItem, TreeView } from '@mui/lab'
Expand Down
2 changes: 1 addition & 1 deletion admin/src/ArrayField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IconBtn, setHidden } from './misc'
import { Add, Edit, Delete } from '@mui/icons-material'
import { confirmDialog, formDialog } from './dialog'
import { DataGrid, GridAlignment } from '@mui/x-data-grid'
import { FieldDescriptor, FieldProps, labelFromKey } from './Form'
import { FieldDescriptor, FieldProps, labelFromKey } from '@hfs/mui-grid-form'
import { Box, FormHelperText, FormLabel } from '@mui/material'

export function ArrayField<T=any>({ label, helperText, fields, value, onChange, onError, ...rest }: FieldProps<T[]> & { fields: FieldDescriptor[], height?: number }) {
Expand Down
3 changes: 1 addition & 2 deletions admin/src/ConfigPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { state, useSnapState } from './state'
import { Info, Refresh } from '@mui/icons-material'
import { Dict, modifiedSx } from './misc'
import { subscribeKey } from 'valtio/utils'
import { Form, BoolField, NumberField, SelectField, FieldProps, Field } from './Form';
import StringStringField from './StringStringField'
import { Form, BoolField, NumberField, SelectField, StringStringField, FieldProps, Field } from '@hfs/mui-grid-form';
import FileField from './FileField'
import { alertDialog, closeDialog, confirmDialog, formDialog, newDialog, toast, waitDialog } from './dialog'
import { proxyWarning } from './HomePage'
Expand Down
4 changes: 2 additions & 2 deletions admin/src/FileField.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { FieldProps, StringField } from './Form'
import { FieldProps, StringField } from '@hfs/mui-grid-form'
import { createElement as h } from 'react'
import { InputAdornment } from '@mui/material'
import { Eject } from '@mui/icons-material'
import { IconBtn } from './misc'
import { newDialog } from '@hfs/shared/lib/dialogs'
import { newDialog } from '@hfs/shared'
import FilePicker from './FilePicker'
import { apiCall } from './api'

Expand Down
2 changes: 1 addition & 1 deletion admin/src/FileForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { state } from './state'
import { createElement as h, isValidElement, useEffect, useMemo, useState } from 'react'
import { Alert, Button } from '@mui/material'
import { BoolField, DisplayField, Field, FieldProps, Form, MultiSelectField, SelectField } from './Form'
import { BoolField, DisplayField, Field, FieldProps, Form, MultiSelectField, SelectField } from '@hfs/mui-grid-form'
import { apiCall, useApiComp } from './api'
import { formatBytes, isEqualLax, modifiedSx, onlyTruthy } from './misc'
import { reloadVfs, VfsNode, Who } from './VfsPage'
Expand Down
2 changes: 1 addition & 1 deletion admin/src/FilePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from '@mui/material'
import { enforceFinal, formatBytes, isWindowsDrive, spinner, Center } from './misc'
import { ArrowUpward, Home } from '@mui/icons-material'
import { StringField } from './Form'
import { StringField } from '@hfs/mui-grid-form'
import { FileIcon, FolderIcon } from './VfsTree'
import { FixedSizeList } from 'react-window'
import AutoSizer from "react-virtualized-auto-sizer"
Expand Down
2 changes: 1 addition & 1 deletion admin/src/InstalledPlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Delete, Error, GitHub, PlayCircle, Settings, StopCircle, SystemUpdateAl
import { IconBtn, xlate } from './misc'
import { formDialog, toast } from './dialog'
import _ from 'lodash'
import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from './Form'
import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from '@hfs/mui-grid-form'
import { ArrayField } from './ArrayField'

export default function InstalledPlugins({ updates }: { updates?: true }) {
Expand Down
2 changes: 1 addition & 1 deletion admin/src/LoginRequired.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { state, useSnapState } from './state'
import { createElement as h, Fragment, useState } from 'react'
import { Center } from './misc'
import { Form } from './Form'
import { Form } from '@hfs/mui-grid-form'
import { apiCall } from './api'
import { srpSequence } from '@hfs/shared'
import { Alert } from '@mui/material'
Expand Down
2 changes: 1 addition & 1 deletion admin/src/MonitorPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Box, Chip } from '@mui/material'
import { DataGrid } from "@mui/x-data-grid"
import { Alert } from '@mui/material'
import { formatBytes, IconBtn, iconTooltip, manipulateConfig, useBreakpoint } from "./misc"
import { Field, SelectField } from './Form'
import { Field, SelectField } from '@hfs/mui-grid-form'
import { GridColumns } from '@mui/x-data-grid/models/colDef/gridColDef'

export default function MonitorPage() {
Expand Down
2 changes: 1 addition & 1 deletion admin/src/OnlinePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DataGrid } from '@mui/x-data-grid'
import { IconBtn } from './misc'
import { Download, Search } from '@mui/icons-material'
import { toast } from './dialog'
import { StringField } from './Form'
import { StringField } from '@hfs/mui-grid-form'
import { useDebounce } from 'use-debounce'
import { repoLink, showError } from './InstalledPlugins'

Expand Down
2 changes: 1 addition & 1 deletion admin/src/PermField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Dict, useStateMounted } from './misc'
import { createElement as h, Fragment, isValidElement } from 'react'
import { Button, Grid } from '@mui/material'
import { Field, FieldProps, SelectField } from './Form'
import { Field, FieldProps, SelectField } from '@hfs/mui-grid-form'
import _ from 'lodash'
import { useApiComp } from './api'

Expand Down
4 changes: 2 additions & 2 deletions admin/src/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
useState
} from 'react'
import { Check, Close, Error as ErrorIcon, Forward, Info, Warning } from '@mui/icons-material'
import { newDialog, closeDialog, dialogsDefaults, DialogOptions } from '@hfs/shared/lib/dialogs'
import { Form, FormProps } from './Form'
import { newDialog, closeDialog, dialogsDefaults, DialogOptions } from '@hfs/shared'
import { Form, FormProps } from '@hfs/mui-grid-form'
export * from '@hfs/shared/lib/dialogs'

dialogsDefaults.Container = function Container(d:DialogOptions) {
Expand Down
19 changes: 19 additions & 0 deletions mui-grid-form/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@hfs/mui-grid-form",
"main": "./lib/Form.js",
"types": "./lib/Form.d.ts",
"scripts": {
"build": "tsc"
},
"dependencies": {
"@mui/icons-material": "*",
"@mui/lab": "*",
"@mui/material": "*"
},
"devDependencies": {
"react": "*",
"react-dom": "*",
"react-scripts": "*",
"typescript": "*"
}
}
218 changes: 6 additions & 212 deletions admin/src/Form.ts → mui-grid-form/src/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,15 @@ import {
useRef,
MutableRefObject
} from 'react'
import {
Box, BoxProps, Button,
Checkbox,
FormControl,
FormControlLabel, FormGroup, FormHelperText,
FormLabel,
Grid, InputAdornment,
MenuItem, Radio,
RadioGroup,
Switch,
TextField
} from '@mui/material'
import { Box, BoxProps, Button, Grid } from '@mui/material'
import { Save } from '@mui/icons-material'
import { LoadingButton } from '@mui/lab'
import _ from 'lodash'
import { SxProps } from '@mui/system'
import { StringField } from './StringField'
export * from './SelectField'
export * from './misc-fields'
export * from './StringStringField'
export { StringField }

type ValidationError = string | boolean // false = no error
export interface FieldDescriptor<T=any> {
Expand Down Expand Up @@ -223,202 +216,3 @@ export interface FieldProps<T> {
error?: true
[rest: string]: any
}

export function StringField({ value, onChange, min, max, required, getApi, typing, start, end, ...props }: FieldProps<string>) {
const setter = () => value ?? ''
getApi?.({
getError() {
return !value && required ? "required"
: value?.length! < min ? "too short"
: value?.length! > max ? "too long"
: false
}
})
const [state, setState] = useState(setter)

const lastChange = useRef(value)
useEffect(() => setState(setter), [value]) //eslint-disable-line
return h(TextField, {
fullWidth: true,
InputLabelProps: state || props.placeholder ? { shrink: true } : undefined,
...props,
value: state,
onChange(ev) {
const val = ev.target.value
setState(val)
if (typing // change state as the user is typing
|| document.activeElement !== ev.target) // autofill ongoing, don't wait onBlur event, just go
go(ev, val)
},
onKeyDown(ev) {
props.onKeyDown?.(ev)
if (ev.key === 'Enter')
go(ev)
},
onBlur(ev) {
props.onBlur?.(ev)
go(ev)
},
InputProps: {
startAdornment: start && h(InputAdornment, { position: 'start' }, start),
endAdornment: end && h(InputAdornment, { position: 'end' }, end),
...props.InputProps,
},
})

function go(event: any, val: string=state) {
const newV = val.trim()
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
lastChange.current = newV
onChange(newV, {
was: value,
event,
cancel() {
setState(setter)
}
})
}
}

export function DisplayField({ value, empty='-', ...props }: any) {
if (!props.toField && empty !== undefined && value !== 0 && !value)
value = empty
return h(StringField, { ...props, value, disabled: true })
}

type SelectOptions<T> = { [label:string]:T } | SelectOption<T>[]
type SelectOption<T> = SelectPair<T> | (T extends string | number ? T : never)
interface SelectPair<T> { label: string, value:T }

export function SelectField<T>(props: FieldProps<T> & { options:SelectOptions<T> }) {
const { value, onChange, getApi, options, sx, ...rest } = props
return h(TextField, { // using TextField because Select is not displaying label correctly
...commonSelectProps(props),
...rest,
onChange(event) {
try {
let newVal: any = event.target.value
newVal = JSON.parse(newVal) as T
onChange(newVal, { was: value, event })
}
catch {}
}
})
}

export function MultiSelectField<T>(props: FieldProps<T[]> & { options:SelectOptions<T> }) {
const { value, onChange, getApi, options, sx, ...rest } = props
return h(TextField, {
...commonSelectProps({ ...props, value: undefined }),
...rest,
SelectProps: { multiple: true },
value: !Array.isArray(value) ? [] : value.map(x => JSON.stringify(x)),
onChange(event) {
try {
let v: any = event.target.value
v = Array.isArray(v) ? v.map(x => JSON.parse(x)) : []
onChange(v as T[], { was: value, event })
}
catch {}
}
})
}

function commonSelectProps<T>(props: { sx?:SxProps, label?: FieldProps<T>['label'], value?: T, disabled?: boolean, options:SelectOptions<T> }) {
const { options, disabled } = props
const normalizedOptions = !Array.isArray(options) ? Object.entries(options).map(([label,value]) => ({ value, label }))
: options.map(o => typeof o === 'string' || typeof o === 'number' ? { value: o, label: String(o) } : o as SelectPair<T>)
const jsonValue = JSON.stringify(props.value)
const currentOption = normalizedOptions.find(x => JSON.stringify(x.value) === jsonValue)
return {
select: true,
fullWidth: true,
sx: props.label ? props.sx : Object.assign({ '& .MuiInputBase-input': { pt: 1 } }, props.sx),
// 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.
value: currentOption ? jsonValue : '',
disabled: !normalizedOptions?.length || disabled,
children: normalizedOptions.map((o, i) => h(MenuItem, {
key: i,
value: JSON.stringify(o?.value),
children: o?.label
}))
}
}

export function NumberField({ value, onChange, getApi, required, min, max, step, ...props }: FieldProps<number | null>) {
getApi?.({
getError() {
return value == null ? (required ? "required" : false)
: (value < min ? "too low" : value > max ? "too high" : false)
}
})
return h(StringField, {
type: 'number',
value: value == null ? '' : String(value),
onChange(v, { was, ...rest }) {
onChange(!v ? null : Number(v), {
...rest,
was: was ? Number(was) : null,
})
},
inputProps: { min, max, step, },
...props,
})
}

export function BoolField({ label='', value, onChange, getApi, helperText, error,
type, // avoid passing this by accident, as it disrupts the control
...props }: FieldProps<boolean>) {
const setter = () => value ?? false
const [state, setState] = useState(setter)
useEffect(() => setState(setter),
[value]) //eslint-disable-line
const control = h(Switch, {
checked: state,
...props,
onChange(event) {
onChange(event.target.checked, { event, was: value })
}
})
return h(Box, { ml: 1, mt: 1, sx: error && { color: 'error.main', outlineOffset: 6, outline: '1px solid' } },
h(FormControlLabel, { label, control, labelPlacement: 'end' }),
helperText && h(FormHelperText, { error }, helperText)
)
}

export function RadioField<T>({ label, options, value, onChange }: FieldProps<T> & { options:SelectPair<T>[] }) {
return h(FormControl, {},
label && h(FormLabel, {}, label),
h(RadioGroup, {
row: true,
name: '',
value: JSON.stringify(value),
onChange(event, v){
onChange(JSON.parse(v), { was: value, event })
}
},
options.map(({ value, label }, idx) =>
h(FormControlLabel, { key: idx, value, control: h(Radio), label }) )
)
)
}

export function CheckboxesField({ label, options, value, onChange }: FieldProps<string[]> & { options: string[] }) {
return h(FormControl, {},
label && h(FormLabel, {}, label),
h(FormGroup, {},
options.map(o => {
const checked = value?.includes(o)
return h(FormControlLabel, {
key: o,
checked,
control: h(Checkbox),
label: o,
onClick(event) {
const newValue = checked ? value!.filter(x => x !== o) : [...value||[], o]
onChange(newValue, { was: value, event })
}
})
}))
)
}
Loading

0 comments on commit 1ba0f55

Please sign in to comment.