fix: MultiSelectField
Massimo Melina committed
Mar 1, 2022 at 12:30 UTC
8baee099a813b96db4c0f8165cd972f8b135f455
2 files changed
+47
-17
admin/src/AccountsPage.ts
+2
-2
@@ -2,7 +2,7 @@ import { isValidElement, createElement as h, useState, useEffect, Fragment } fro
2
import { apiCall, useApiComp } from './api'
3
import { Box, Button, Card, CardContent, Grid, List, ListItem, ListItemText, Typography } from '@mui/material'
4
import { Delete, Group, Person, PersonAdd, Refresh } from '@mui/icons-material'
5
-import { BoolField, Form, SelectField, StringField } from './Form'
5
+import { BoolField, Form, MultiSelectField, SelectField, StringField } from './Form'
6
import { alertDialog, confirmDialog } from './dialog'
7
import { isEqualLax, onlyTruthy } from './misc'
8
import { TreeItem, TreeView } from '@mui/lab'
@@ -148,7 +148,7 @@ function AccountForm({ account, done, groups }: { account: Account, groups: stri
148
{ k: 'ignore_limits', comp: BoolField,
149
helperText: values.ignore_limits ? "Speed limits don't apply to this account" : "Speed limits apply to this account" },
150
{ k: 'redirect', comp: StringField, helperText: "If you want this account to be redirected to a specific folder/address at login time" },
151
- { k: 'belongs', comp: SelectField, multiple: true, label: "Inherits from", options: belongsOptions,
151
+ { k: 'belongs', comp: MultiSelectField, label: "Inherits from", options: belongsOptions,
152
helperText: "Options and permissions of the selected groups will be applied to this account. "
153
+ (belongsOptions.length ? '' : "There are no groups available, create one first.") }
154
],
admin/src/Form.ts
+45
-15
@@ -17,6 +17,9 @@ import _ from 'lodash'
17
18
interface FieldDescriptor { k:string, comp?: any, label?: string | ReactElement, [extraProp:string]:any }
19
20
+// it seems necessary to cast (Multi)SelectField sometimes
21
+export type FieldComponent<T> = (props:FieldProps<T>) => ReactElement
22
+
23
interface FormProps {
24
fields: (FieldDescriptor | ReactElement | null | undefined | false)[]
25
defaults?: (f:FieldDescriptor) => Dict | void
@@ -130,25 +133,19 @@ export function DisplayField({ map, value, empty='-', ...props }: any) {
133
}
134
135
interface SelectPair<T> { label: string, value:T }
133
-export function SelectField<T>({ value, onChange, options, multiple, ...props }: FieldProps<T> & { options:SelectPair<T>[] }) {
136
+export function SelectField<T>(props: FieldProps<T> & { options:SelectPair<T>[] }) {
137
+ const { value, onChange, options, ...rest } = props
138
+ const jsonValue = JSON.stringify(value)
139
+ const currentOption = options.find(x => JSON.stringify(x.value) === jsonValue)
140
return h(TextField, { // using TextField because Select is not displaying label correctly
135
- ...props,
136
- select: true,
137
- fullWidth: true,
138
- disabled: !options?.length || props.disabled,
139
- SelectProps: multiple && { multiple: true },
140
- value: multiple ? (!Array.isArray(value) ? [] : value.map(x => JSON.stringify(x)))
141
- : value === undefined ? '' : JSON.stringify(value),
142
- children: options.map((o,i) => {
143
- const obj = o && typeof o === 'object'
144
- const value = obj && 'value' in o ? o.value : o
145
- const label = obj && 'label' in o ? o.label : o
146
- return h(MenuItem, { key: i, value: JSON.stringify(value), children: label })
147
- }),
141
+ ...rest,
142
+ ...commonSelectProps(props),
143
+ // 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.
144
+ value: currentOption ? jsonValue : '',
145
onChange(event) {
146
try {
147
let newVal: any = event.target.value
151
- newVal = multiple && Array.isArray(newVal) ? newVal.map(x => JSON.parse(x)) : JSON.parse(newVal) as T
148
+ newVal = JSON.parse(newVal) as T
149
onChange(newVal, { was: value, event })
150
}
151
catch {}
@@ -156,6 +153,39 @@ export function SelectField<T>({ value, onChange, options, multiple, ...props }:
153
})
154
}
155
156
+export function MultiSelectField<T>(props: FieldProps<T[]> & { options:SelectPair<T>[] }) {
157
+ const { value, onChange, options, ...rest } = props
158
+ return h(TextField, {
159
+ ...rest,
160
+ ...commonSelectProps(props),
161
+ SelectProps: { multiple: true },
162
+ value: !Array.isArray(value) ? [] : value.map(x => JSON.stringify(x)),
163
+ onChange(event) {
164
+ try {
165
+ let v: any = event.target.value
166
+ v = Array.isArray(v) ? v.map(x => JSON.parse(x)) : []
167
+ onChange(v as T[], { was: value, event })
168
+ }
169
+ catch {}
170
+ }
171
+ })
172
+}
173
+
174
+function commonSelectProps<T>(props: { disabled?: boolean, options:SelectPair<T>[] }) {
175
+ const { options, disabled } = props
176
+ return {
177
+ select: true,
178
+ fullWidth: true,
179
+ disabled: !options?.length || disabled,
180
+ children: options.map((o, i) => {
181
+ const obj = o && typeof o === 'object'
182
+ const value = obj && 'value' in o ? o.value : o
183
+ const label = obj && 'label' in o ? o.label : o
184
+ return h(MenuItem, { key: i, value: JSON.stringify(value), children: label })
185
+ })
186
+ }
187
+}
188
+
189
export function NumberField({ value, onChange, ...props }: FieldProps<number | null>) {
190
// @ts-ignore
191
return h(StringField, {