fix: admin/accounts: bad mobile ux
Massimo Melina committed
Feb 3, 2023 at 16:30 UTC
3ca5b93fec876f3191b49d70eafcd002ad1e688e
2 files changed
+80
-70
admin/src/AccountForm.ts
+23
-11
@@ -1,22 +1,25 @@
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 { createElement as h, useEffect, useRef, useState } from 'react'
3
+import { createElement as h, ReactNode, useEffect, useRef, useState } from 'react'
4
import { BoolField, Form, MultiSelectField } from '@hfs/mui-grid-form'
5
-import { Box, Button } from '@mui/material'
5
+import { Box } from '@mui/material'
6
import { apiCall } from './api'
7
-import { alertDialog } from './dialog'
8
-import { isEqualLax, modifiedSx } from './misc'
7
+import { alertDialog, toast, useDialogBarColors } from './dialog'
8
+import { IconBtn, isEqualLax, modifiedSx } from './misc'
9
import { Account, account2icon } from './AccountsPage'
10
import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
11
+import { Delete } from '@mui/icons-material'
12
+import { isMobile } from '@hfs/frontend/src/misc'
13
12
-interface FormProps { account: Account, groups: string[], done: (username: string)=>void, close: ()=>void }
13
-export default function AccountForm({ account, done, groups, close }: FormProps) {
14
+interface FormProps { account: Account, groups: string[], done: (username: string)=>void, reload: ()=>void, addToBar: ReactNode }
15
+export default function AccountForm({ account, done, groups, addToBar, reload }: FormProps) {
16
const [values, setValues] = useState<Account & { password?: string, password2?: string }>(account)
17
const [belongsOptions, setBelongOptions] = useState<string[]>([])
18
useEffect(() => {
19
setValues(account)
20
setBelongOptions(groups.filter(x => x !== account.username ))
19
- ref.current?.querySelector('input')?.focus()
21
+ if (!isMobile())
22
+ ref.current?.querySelector('input')?.focus()
23
}, [JSON.stringify(account)]) //eslint-disable-line
24
const add = !account.username
25
const group = !values.hasPassword
@@ -27,8 +30,16 @@ export default function AccountForm({ account, done, groups, close }: FormProps)
30
set(v, k) {
31
setValues({ ...values, [k]: v })
32
},
33
+ barSx: { gap: 2, width: '100%', ...useDialogBarColors() },
34
+ stickyBar: true,
35
addToBar: [
31
- h(Button, { onClick: close, sx: { ml: 2 } }, "Close"),
36
+ !add && h(IconBtn, {
37
+ icon: Delete,
38
+ title: "Delete",
39
+ confirm: "Delete?",
40
+ onClick: () => apiCall('del_account', { username: account.username }).then(() => reload())
41
+ }),
42
+ addToBar,
43
h(Box, { flex:1 }),
44
account2icon(values, { fontSize: 'large', sx: { p: 1 }})
45
],
@@ -69,7 +80,8 @@ export default function AccountForm({ account, done, groups, close }: FormProps)
80
throw e
81
}
82
done(got.username)
72
- return alertDialog("Account created", 'success')
83
+ toast("Account created", 'success')
84
+ return
85
}
86
const got = await apiCall('set_account', {
87
username: account.username,
@@ -77,8 +89,8 @@ export default function AccountForm({ account, done, groups, close }: FormProps)
89
})
90
if (password)
91
await apiNewPassword(username, password)
80
- done(got.username)
81
- return alertDialog("Account modified", 'success')
92
+ setTimeout(() => toast("Account modified", 'success'), 1) // workaround: showing a dialog at this point is causing a crash if we are in a dialog
93
+ done(got.username) // username may have been changed, so we pass it back
94
}
95
}
96
})
admin/src/AccountsPage.ts
+57
-59
@@ -1,15 +1,15 @@
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 { createElement as h, useState, useEffect, Fragment } from "react"
4
-import { apiCall, useApiEx } from './api'
5
-import { Alert, Box, Button, Card, CardContent, Grid, List, ListItem, ListItemText, Typography } from '@mui/material'
6
-import { Delete, Group, MilitaryTech, Person, PersonAdd, Refresh } from '@mui/icons-material'
7
-import { alertDialog, confirmDialog } from './dialog'
8
-import { iconTooltip, onlyTruthy } from './misc'
4
+import { useApiEx } from './api'
5
+import { Alert, Box, Card, CardContent, Grid, List, ListItem, ListItemText, Typography } from '@mui/material'
6
+import { Close, Group, MilitaryTech, Person, PersonAdd, Refresh } from '@mui/icons-material'
7
+import { IconBtn, iconTooltip, newDialog, useBreakpoint } from './misc'
8
import { TreeItem, TreeView } from '@mui/lab'
9
import MenuButton from './MenuButton'
10
import AccountForm from './AccountForm'
11
import md from './md'
12
+import _ from 'lodash'
13
14
export interface Account {
15
username: string
@@ -29,10 +29,51 @@ export default function AccountsPage() {
29
if (Array.isArray(data?.list) && selectionMode)
30
setSel( sel.filter(u => data.list.find((e:any) => e?.username === u)) ) // remove elements that don't exist anymore
31
}, [data]) //eslint-disable-line -- Don't fall for its suggestion to add `sel` here: we modify it and declaring it as a dependency would cause a logical loop
32
- if (element)
33
- return element
34
- const { list }: { list: Account[] } = data
35
- return h(Grid, { container: true, maxWidth: '80em' },
32
+ const list: Account[] | undefined = data?.list
33
+ const selectedAccount = selectionMode && _.find(list, { username: sel[0] })
34
+
35
+ function close() {
36
+ setSel([])
37
+ }
38
+
39
+ const sideBreakpoint = 'md'
40
+ const isSideBreakpoint = useBreakpoint(sideBreakpoint)
41
+
42
+ const sideContent = !(sel.length > 0) || !list ? null // this clever test is true both when some accounts are selected and when we are in "new account" modes
43
+ : selectionMode && sel.length > 1 ? h(Fragment, {},
44
+ h(Typography, {}, sel.length + " selected"),
45
+ h(List, {},
46
+ sel.map(username =>
47
+ h(ListItem, { key: username },
48
+ h(ListItemText, {}, username))))
49
+ )
50
+ : h(AccountForm, {
51
+ account: selectedAccount || { username: '', hasPassword: sel === 'new-user' },
52
+ groups: list.filter(x => !x.hasPassword).map( x => x.username ),
53
+ addToBar: isSideBreakpoint && h(IconBtn, { // not really useful, but users misled in thinking it's a dialog will find satisfaction in dismissing the form
54
+ icon: Close,
55
+ title: "Close",
56
+ onClick: close
57
+ }),
58
+ reload,
59
+ done(username) {
60
+ setSel([username])
61
+ reload()
62
+ }
63
+ })
64
+ useEffect(() => {
65
+ if (isSideBreakpoint || !sideContent || !sel.length) return
66
+ return newDialog({
67
+ title: _.isString(sel) ? _.startCase(sel)
68
+ : sel.length > 1 ? "Multiple selection"
69
+ : selectedAccount ? (selectedAccount.hasPassword ? "User: " : "Group: ") + selectedAccount.username
70
+ : '?', // never
71
+ Content: () => sideContent,
72
+ onClose: close,
73
+ })
74
+ }, [isSideBreakpoint, sel, selectedAccount])
75
+
76
+ return element || h(Grid, { container: true, maxWidth: '80em' },
77
h(Grid, { item: true, xs: 12 },
78
h(Box, {
79
display: 'flex',
@@ -54,26 +95,12 @@ export default function AccountsPage() {
95
{ children: "user", onClick: () => setSel('new-user') },
96
{ children: "group", onClick: () => setSel('new-group') }
97
]
57
- }, 'Add'),
58
- h(Button, {
59
- disabled: !selectionMode || !sel.length,
60
- startIcon: h(Delete),
61
- async onClick(){
62
- if (!selectionMode) return
63
- if (!await confirmDialog(`You are going to delete ${sel.length} account(s)`))
64
- return
65
- const errors = onlyTruthy(await Promise.all(sel.map(username =>
66
- apiCall('del_account', { username }).then(() => null, () => username) )))
67
- if (errors.length)
68
- return alertDialog(errors.length === sel.length ? "Request failed" : hList("Some accounts were not deleted", errors), 'error')
69
- reload()
70
- }
71
- }, "Remove"),
72
- h(Button, { onClick: reload, startIcon: h(Refresh) }, "Reload"),
73
- list.length > 0 && h(Typography, { p: 1 }, `${list.length} account(s)`),
98
+ }, "Add"),
99
+ h(IconBtn, { icon: Refresh, title: "Reload", onClick: reload }),
100
+ list?.length! > 0 && h(Typography, { p: 1 }, `${list!.length} account(s)`),
101
) ),
102
h(Grid, { item: true, md: 5 },
76
- !list.length && h(Alert, { severity: 'info' }, md`To access administration _remotely_ you will need to create a user account with admin permission`),
103
+ !list?.length && h(Alert, { severity: 'info' }, md`To access administration _remotely_ you will need to create a user account with admin permission`),
104
h(TreeView, {
105
multiSelect: true,
106
sx: { pr: 4, pb: 2, minWidth: '15em' },
@@ -82,7 +109,7 @@ export default function AccountsPage() {
109
setSel(ids)
110
}
111
},
85
- list.map((ac: Account) =>
112
+ list?.map((ac: Account) =>
113
h(TreeItem, {
114
key: ac.username,
115
nodeId: ac.username,
@@ -105,37 +132,8 @@ export default function AccountsPage() {
132
)
133
)
134
),
108
- sel.length > 0 // this clever test is true both when some accounts are selected and when we are in "new account" modes
109
- && h(Grid, { item: true, md: 7 },
110
- h(Card, {},
111
- h(CardContent, {},
112
- selectionMode && sel.length > 1 ? h(Box, {},
113
- h(Typography, {}, sel.length + " selected"),
114
- h(List, {},
115
- sel.map(username =>
116
- h(ListItem, { key: username },
117
- h(ListItemText, {}, username))))
118
- ) : h(AccountForm, {
119
- account: selectionMode && list.find(x => x.username === sel[0])
120
- || { username: '', hasPassword: sel === 'new-user' },
121
- groups: list.filter(x => !x.hasPassword).map( x => x.username ),
122
- close(){ setSel([]) },
123
- done(username) {
124
- setSel([username])
125
- reload()
126
- }
127
- })
128
- )))
129
- )
130
-}
131
-
132
-function hList(heading: string, list: any[]) {
133
- return h(Fragment, {},
134
- heading>'' && h(Typography, {}, heading),
135
- h(List, {},
136
- list.map((text,key) =>
137
- h(ListItem, { key },
138
- typeof text === 'string' ? h(ListItemText, {}, text) : text) ))
135
+ isSideBreakpoint && sideContent && h(Grid, { item: true, md: 7 },
136
+ h(Card, {}, h(CardContent, {}, sideContent) )),
137
)
138
}
139