@samitouri / QOSami-HFS / commits / 5ab119fc

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