@samitouri / QOSami-HFS / commits / 4bd87d01

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),