fix: plugins: if username is the first field of a config, focus was automatically placed on second field
Massimo Melina committed
May 30, 2024 at 18:45 UTC
5ab119fced79f1b718b9726acee89024f70ede52
3 files changed
+22
-17
admin/src/InstalledPlugins.ts
+2
-2
@@ -192,8 +192,8 @@ export async function startPlugin(id: string) {
192
}
193
194
function UsernameField({ value, onChange, ...rest }: FieldProps<string>) {
195
- const { data, element } = useApiEx<{ list: Account[] }>('get_accounts')
196
- return element || h(SelectField as Field<string>, {
195
+ const { data, element, loading } = useApiEx<{ list: Account[] }>('get_accounts')
196
+ return !loading && element || h(SelectField as Field<string>, {
197
value, onChange,
198
options: data?.list.map(x => x.username),
199
helperText: "Only users, no groups here",
admin/src/dialog.ts
+2
-2
@@ -8,7 +8,7 @@ import {
8
} from 'react'
9
import { Check, Close, Error as ErrorIcon, Forward, Info, Warning } from '@mui/icons-material'
10
import { newDialog, closeDialog, dialogsDefaults, DialogOptions, componentOrNode, pendingPromise,
11
- focusSelector, md } from '@hfs/shared'
11
+ focusSelector, md, focusableSelector } from '@hfs/shared'
12
import { Form, FormProps } from '@hfs/mui-grid-form'
13
import { IconBtn, Flex, Center } from './mui'
14
import { useDark } from './theme'
@@ -28,7 +28,7 @@ dialogsDefaults.Container = function Container(d: DialogOptions) {
28
if (!el) return
29
el.focus()
30
if (mobile) return
31
- focusSelector('[autofocus]', el) || focusSelector('input,textarea', el)
31
+ focusSelector('[autofocus]', el) || focusSelector(focusableSelector, el)
32
})
33
return () => clearTimeout(h)
34
}, [ref.current])
mui-grid-form/SelectField.ts
+18
-13
@@ -2,7 +2,7 @@
2
3
import { createElement as h, Fragment, ReactNode, useId, useMemo } from 'react'
4
import { FieldProps } from '.'
5
-import { FormControl, FormControlLabel, FormLabel, MenuItem, Radio, InputLabel, Select,
5
+import { FormControl, FormControlLabel, FormLabel, MenuItem, Radio, InputLabel, Select, LinearProgress,
6
ListItemText, Checkbox, FilledInput, RadioGroup, TextField, FormHelperText, Button } from '@mui/material'
7
import { SxProps } from '@mui/system'
8
@@ -14,14 +14,14 @@ export function SelectField<T>(props: FieldProps<T> & CommonSelectProps<T>) {
14
const { value, onChange, setApi, options, sx, disabled, ...rest } = props
15
const normalizedOptions = useMemo(() => normalizeOptions(options), [options])
16
const jsonValue = JSON.stringify(value)
17
- const currentOption = normalizedOptions.find(x => JSON.stringify(x.value) === jsonValue)
17
+ const currentOption = normalizedOptions?.find(x => JSON.stringify(x.value) === jsonValue)
18
return h(TextField, { // using TextField because Select is not displaying label correctly
19
select: true,
20
hiddenLabel: !props.label,
21
// 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.
22
value: currentOption ? jsonValue : '',
23
- disabled: !normalizedOptions?.length || disabled,
24
- children: normalizedOptions.map((o, i) => h(MenuItem, {
23
+ disabled: normalizedOptions?.length === 0 || disabled,
24
+ children: !normalizedOptions ? h(LinearProgress) : normalizedOptions.map((o, i) => h(MenuItem, {
25
key: i,
26
value: JSON.stringify(o?.value),
27
children: h(Fragment, { key: i }, o?.label) // without this fragment/key, a label as h(span) will produce warnings
@@ -47,7 +47,7 @@ export function MultiSelectField<T>({ renderOption, ...props }: MultiSelectField
47
const { value, onChange, setApi, options, placeholder, helperText, label, valueSeparator = ', ', clearable=true, ...rest } = props
48
const normalizedOptions = useMemo(() => normalizeOptions(options), [options])
49
const valueAsOptions = useMemo(() => !Array.isArray(value) ? []
50
- : value.map(x => normalizedOptions.find(o => o.value === x) || { value: x, label: String(x) }),
50
+ : value.map(x => normalizedOptions?.find(o => o.value === x) || { value: x, label: String(x) }),
51
[value, normalizedOptions])
52
const valueAsJsons = useMemo(() => value?.map(x => JSON.stringify(x)) || [], [value])
53
const labelId = useId()
@@ -80,18 +80,22 @@ export function MultiSelectField<T>({ renderOption, ...props }: MultiSelectField
80
}),
81
...rest,
82
},
83
- !normalizedOptions.length && h(ListItemText, { sx: { fontStyle: 'italic', ml: 1 }, onClickCapture(ev) { ev.stopPropagation() } }, "No options available"),
84
- normalizedOptions.length > 1 && h(Button, {
83
+ !normalizedOptions ? h(LinearProgress)
84
+ : (!normalizedOptions.length && h(ListItemText, {
85
+ sx: { fontStyle: 'italic', ml: 1 },
86
+ onClickCapture(ev) { ev.stopPropagation() }
87
+ }, "No options available")),
88
+ normalizedOptions?.length! > 1 && h(Button, {
89
ref: x => x && Object.assign(x, { role: undefined }), // cancel the role=option on this
90
onClickCapture(event) {
91
event.stopPropagation()
88
- onChange(showClear ? [] : normalizedOptions.map(x => x.value), { was: value, event })
92
+ onChange(showClear ? [] : normalizedOptions!.map(x => x.value), { was: value, event })
93
},
94
}, showClear ? "Unselect all" : "Select all"),
91
- ...normalizedOptions.map(o => h(MenuItem, { value: JSON.stringify(o?.value) }, // encode, as this supports only string|number
95
+ ...normalizedOptions?.map(o => h(MenuItem, { value: JSON.stringify(o?.value) }, // encode, as this supports only string|number
96
h(Checkbox, { checked: value?.includes(o.value) || false }),
97
h(ListItemText, { primary: renderOption?.(o) ?? o.label })
94
- )),
98
+ )) || [],
99
),
100
h(FormHelperText, { id: helperId, error: props.error }, helperText),
101
)
@@ -101,7 +105,8 @@ type HelperCommon<T> = Pick<FieldProps<T>, 'value' | 'onChange' | 'label'>
105
interface CommonSelectProps<T> extends HelperCommon<T> {
106
sx?: SxProps
107
disabled?: boolean
104
- options: SelectOptions<T>
108
+ // pass options undefined to display a loading indicator in place of the options
109
+ options?: SelectOptions<T>
110
}
111
function commonSelectProps<T>(props: CommonSelectProps<T>) {
112
return {
@@ -115,8 +120,8 @@ function commonSelectProps<T>(props: CommonSelectProps<T>) {
120
}
121
}
122
118
-function normalizeOptions<T>(options: SelectOptions<T>) {
119
- return !Array.isArray(options) ? Object.entries(options).map(([label,value]) => ({ value, label }))
123
+function normalizeOptions<T>(options?: SelectOptions<T>) {
124
+ return !options ? undefined : !Array.isArray(options) ? Object.entries(options).map(([label,value]) => ({ value, label }))
125
: options.map(o => typeof o === 'string' || typeof o === 'number' ? { value: o, label: String(o) } : o as SelectPair<T>)
126
}
127