admin/internet: better picker for countries
Massimo Melina committed
Nov 29, 2023 at 10:26 UTC
4bd87d01fabbf8e793d9feaedef55b93c28cf2b0
2 files changed
+40
-21
admin/src/InternetPage.ts
+1
-1
@@ -51,7 +51,7 @@ export default function InternetPage() {
51
function geoBox() {
52
const countryOptions = useMemo(() => _.sortBy(COUNTRIES, 'name').map(x => ({
53
value: x.code,
54
- label: h('span', { style: { whiteSpace: 'nowrap' } }, `${x.flag} ${x.name}`)
54
+ label: `${x.flag} ${x.name}`
55
})), [COUNTRIES])
56
return h(TitleCard, { title: "Geo IP", icon: Public },
57
h(ConfigForm<{
mui-grid-form/SelectField.ts
+39
-20
@@ -1,10 +1,9 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { createElement as h, Fragment, ReactNode } from 'react'
3
+import { createElement as h, Fragment, ReactNode, useMemo } from 'react'
4
import { FieldProps } from '.'
5
-import { FormControl, FormControlLabel, FormLabel, IconButton, InputAdornment, MenuItem, Radio, RadioGroup,
6
- StandardTextFieldProps, TextField, Tooltip
7
-} from '@mui/material'
5
+import { Autocomplete, FormControl, FormControlLabel, FormLabel, IconButton, InputAdornment, MenuItem, Radio,
6
+ RadioGroup, StandardTextFieldProps, TextField, Tooltip } from '@mui/material'
7
import { SxProps } from '@mui/system'
8
import { Clear } from '@mui/icons-material'
9
@@ -29,20 +28,36 @@ export function SelectField<T>(props: FieldProps<T> & CommonSelectProps<T>) {
28
}
29
30
export function MultiSelectField<T>(props: FieldProps<T[]> & CommonSelectProps<T>) {
32
- const { value, onChange, setApi, options, sx, clearable, clearValue, ...rest } = props
33
- return h(TextField, {
34
- ...commonSelectProps({ clearValue: [], ...props }),
35
- ...rest,
36
- SelectProps: { multiple: true },
37
- sx: { ...rest.sx, '& div[role=button]': { whiteSpace: 'unset' } },
38
- value: !Array.isArray(value) ? [] : value.map(x => JSON.stringify(x)),
39
- onChange(event) {
40
- try {
41
- let v: any = event.target.value
42
- v = Array.isArray(v) ? v.map(x => JSON.parse(x)) : []
43
- onChange(v as T[], { was: value, event })
44
- }
45
- catch {}
31
+ const { value, onChange, setApi, options, sx, clearable, clearValue, placeholder, ...rest } = props
32
+ const { select, InputProps, ...common } = commonSelectProps({ clearValue: [], ...props, clearable: false })
33
+ const normalizedOptions = useMemo(() => normalizeOptions(options), [options])
34
+ const valueAsOptions = useMemo(() => !Array.isArray(value) ? []
35
+ : value.map(x => normalizedOptions.find(o => o.value === x) || { value: x, label: String(x) }),
36
+ [value, normalizedOptions])
37
+ return h(Autocomplete<SelectPair<T>, true>, {
38
+ multiple: true,
39
+ options: normalizedOptions,
40
+ filterSelectedOptions: true,
41
+ onChange: (event, sel) => onChange(sel.map(x => x.value) as T[], { was: value, event }),
42
+ isOptionEqualToValue: (option, val) => option.value === val.value,
43
+ getOptionLabel: x => x.label,
44
+ renderOption: (props, x) => h('span', props, x.label),
45
+ ...common,
46
+ value: valueAsOptions,
47
+ renderInput: params => h(TextField, {
48
+ ...rest,
49
+ placeholder: valueAsOptions.length ? undefined : placeholder, // TextField's own logic doesn't know about the main field not being empty
50
+ SelectProps: { multiple: true },
51
+ sx: { ...rest.sx, '& div[role=button]': { whiteSpace: 'unset' } },
52
+ ...params,
53
+ }),
54
+ sx: {
55
+ '.MuiAutocomplete-tag': { height: 24 }, // too tall, otherwise
56
+ '.MuiAutocomplete-inputRoot': { pt: '21px' }, // some extra margin from label
57
+ 'input[type][type]': { p: '4px' },
58
+ '.MuiChip-deleteIcon[class]': { position: 'absolute', right: '-0.6em', opacity: 0, color: 'text.primary', transition: 'all .2s' },
59
+ '.MuiChip-root:hover .MuiChip-deleteIcon': { opacity: 1 },
60
+ ...sx,
61
}
62
})
63
}
@@ -59,8 +74,7 @@ interface CommonSelectProps<T> extends HelperCommon<T> {
74
}
75
function commonSelectProps<T>(props: CommonSelectProps<T>) {
76
const { options, disabled, start, end, clearable, clearValue, value } = props
62
- const normalizedOptions = !Array.isArray(options) ? Object.entries(options).map(([label,value]) => ({ value, label }))
63
- : options.map(o => typeof o === 'string' || typeof o === 'number' ? { value: o, label: String(o) } : o as SelectPair<T>)
77
+ const normalizedOptions = normalizeOptions(options)
78
const jsonValue = JSON.stringify(value)
79
const currentOption = normalizedOptions.find(x => JSON.stringify(x.value) === jsonValue)
80
const showClear = clearable && (Array.isArray(value) ? value.length > 0 : value)
@@ -90,6 +104,11 @@ function commonSelectProps<T>(props: CommonSelectProps<T>) {
104
}
105
}
106
107
+function normalizeOptions<T>(options: SelectOptions<T>) {
108
+ return !Array.isArray(options) ? Object.entries(options).map(([label,value]) => ({ value, label }))
109
+ : options.map(o => typeof o === 'string' || typeof o === 'number' ? { value: o, label: String(o) } : o as SelectPair<T>)
110
+}
111
+
112
export function RadioField<T>({ label, options, value, onChange }: FieldProps<T> & { options:SelectPair<T>[] }) {
113
return h(FormControl, {},
114
label && h(FormLabel, {}, label),