@samitouri / QOSami-HFS / commits / e5970371

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