admin/plugins: better 'username' config field, sorted and with icons
Massimo Melina committed
Mar 20, 2026 at 18:58 UTC
e5970371e304826996fc4eede3d847a1c61b62ea
2 files changed
+22
-8
admin/src/InstalledPlugins.ts
+16
-4
@@ -1,11 +1,12 @@
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 { apiCall, useApiEx, useApiList } from './api'
4
-import { createElement as h, Fragment, useEffect, useState } from 'react'
4
+import { createElement as h, Fragment, useEffect, useMemo, useState } from 'react'
5
+import { Account, account2icon } from './AccountsPage'
6
import { Box, Breakpoint, Link, Paper, Table, TableCell, TableRow, useTheme } from '@mui/material'
7
import { DataTable, DataTableColumn } from './DataTable'
8
import {
8
- Clear, Delete, Error as ErrorIcon, FormatPaint as ThemeIcon, ListAlt, PlayCircle, Settings, StopCircle, Upgrade
9
+ Clear, Delete, Error as ErrorIcon, FormatPaint as ThemeIcon, ListAlt, MilitaryTech, PlayCircle, Settings, StopCircle, Upgrade
10
} from '@mui/icons-material'
11
import {
12
CFG, Html, HTTP_FAILED_DEPENDENCY, md, newObj, prefix, with_, xlate, formatTime, formatDate, replaceStringToReact,
@@ -318,9 +319,20 @@ export async function startPlugin(id: string) {
319
320
function UsernameField({ value, onChange, multiple, groups, ...rest }: FieldProps<string>) {
321
const { data, element, loading } = useApiEx<typeof adminApis.get_accounts>('get_accounts')
322
+ const list = useMemo(() => data && _.sortBy(data.list, [x => !x.isGroup, x => !x.adminActualAccess, 'username']), [data])
323
+ type UsernameOption = { value: string, label: string, a: Account }
324
return !loading && element || h((multiple ? MultiSelectField : SelectField) as Field<string>, {
325
value, onChange,
323
- options: data?.list.filter(x => groups === undefined || groups === x.isGroup).map(x => x.username),
326
+ options: list?.filter(x => groups === undefined || groups === x.isGroup).map(a => ({ value: a.username, label: a.username, a })),
327
+ renderOption: (x: UsernameOption) => {
328
+ const icon = x.a.isGroup && account2icon(x.a) || x.a.adminActualAccess && iconTooltip(MilitaryTech, "Can login into Admin")
329
+ if (!icon)
330
+ return x.label
331
+ return !icon ? x.label
332
+ : h('span', {},
333
+ h('span', { style: { marginLeft: -8, marginRight: 8 } }, icon),
334
+ x.label)
335
+ },
336
...rest,
337
})
338
}
@@ -367,4 +379,4 @@ export const themeField: DataTableColumn = {
379
renderCell({ value }) {
380
return value && iconTooltip(ThemeIcon, _.isString(value) ? `${value} theme` : "theme", { fontSize: '1.2rem', mr: '.3em' })
381
}
370
-}
\ No newline at end of file
382
+}
mui-grid-form/SelectField.ts
+6
-4
@@ -12,12 +12,14 @@ import { useIsMobile } from '@hfs/shared'
12
type SelectOptions<T> = { [label:string]: T } | SelectOption<T>[]
13
type SelectOption<T> = SelectOptionNormalized<T> | (T extends string | number ? T : never)
14
interface SelectOptionNormalized<T> { label?: string, value: T, disabled?: boolean }
15
+type RenderOption<T> = (option: SelectOptionNormalized<T>) => ReactNode
16
16
-export function SelectField<T>(props: FieldProps<T> & CommonSelectProps<T> & { defaultValue?: T }) {
17
- const { defaultValue, value=defaultValue, onChange, setApi, options, sx, disabled, afterList, ...rest } = props
17
+export function SelectField<T>(props: FieldProps<T> & CommonSelectProps<T> & { defaultValue?: T, renderOption?: RenderOption<T> }) {
18
+ let { defaultValue, value=defaultValue, onChange, setApi, options, sx, disabled, afterList, renderOption, ...rest } = props
19
const normalizedOptions = useMemo(() => normalizeOptions(options), [options])
20
const jsonValue = JSON.stringify(value)
21
const currentOption = normalizedOptions?.find(x => JSON.stringify(x.value) === jsonValue)
22
+ renderOption ??= x => x.label ?? String(x.value)
23
return h(TextField, { // using TextField because Select is not displaying label correctly
24
select: true,
25
hiddenLabel: !props.label,
@@ -29,7 +31,7 @@ export function SelectField<T>(props: FieldProps<T> & CommonSelectProps<T> & { d
31
key: i,
32
value: JSON.stringify(o?.value),
33
disabled: o?.disabled,
32
- children: h(Fragment, { key: i }, o?.label ?? String(o?.value)) // without this fragment/key, a label as h(span) will produce warnings
34
+ children: h(Fragment, { key: i }, renderOption!(o)) // without this fragment/key, a label as h(span) will produce warnings
35
})),
36
h('div', { key: -1 }, afterList),
37
],
@@ -47,7 +49,7 @@ export function SelectField<T>(props: FieldProps<T> & CommonSelectProps<T> & { d
49
}
50
51
type MultiSelectFieldProps<T> = FieldProps<T[]> & CommonSelectProps<T> & {
50
- renderOption?: (option: SelectOptionNormalized<T>) => ReactNode
52
+ renderOption?: RenderOption<T>
53
clearable?: boolean
54
}
55
export function MultiSelectField<T>({ renderOption, ...props }: MultiSelectFieldProps<T>) {