admin/account: animated save button

Massimo Melina committed Apr 27, 2026 at 14:12 UTC 7cefd4ffd4cda3ba95b6fa9017a659016da99a06
3 files changed +14 -7
admin/src/AccountForm.ts
+10 -4
@@ -13,8 +13,13 @@ import { state, useSnapState } from './state'
13 import VfsPathField from './VfsPathField'
14 import { DateTimeField } from './DateTimeField'
15
16 -interface FormProps { account: Account, groups: string[], done: (username: string)=>void, reload: ()=>void, addToBar: ReactNode }
17 -export default function AccountForm({ account, done, groups, addToBar, reload }: FormProps) {
16 +export default function AccountForm({ account, done, groups, addToBar, reload }: {
17 + account: Account,
18 + groups: string[],
19 + done: (username: string, saveBtn?: HTMLButtonElement) => void,
20 + reload: () => void,
21 + addToBar: ReactNode
22 +}) {
23 const { username } = useSnapState()
24 const [values, setValues] = useState<Account & { password?: string, password2?: string }>(account)
25 const [belongsOptions, setBelongOptions] = useState<string[]>([])
@@ -117,6 +122,7 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
122 ...propsForModifiedValues(isModifiedConfig(values, account)),
123 async onClick() {
124 const { password='', password2, adminActualAccess, hasPassword, invalidated, canLogin, members, ...withoutPassword } = values
125 + const saveBtn = ref.current?.querySelector<HTMLButtonElement>('button.saveBtn') || undefined
126 if (add) {
127 const got = await apiCall('add_account', withoutPassword)
128 if (password)
@@ -125,7 +131,7 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
131 void apiCall('del_account', { username: values.username }) // best effort, don't wait
132 throw e
133 }
128 - done(got?.username)
134 + done(got?.username, saveBtn)
135 return
136 }
137 const got = await apiCall('set_account', {
@@ -136,7 +142,7 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
142 await apiNewPassword(values.username, password)
143 if (account.username === username)
144 state.username = values.username
139 - done(got?.username) // username may have been changed, so we pass it back
145 + done(got?.username, saveBtn) // username may have been changed, so we pass it back
146 }
147 }
148 })
admin/src/AccountsPage.ts
+3 -3
@@ -7,7 +7,7 @@ import {
7 AccountTree, ChevronRight, Close, Delete, DoNotDisturb, ExpandMore, Group, MilitaryTech, Person, PersonAdd, Schedule
8 } from '@mui/icons-material'
9 import { newDialog, with_, md, Jsonify } from './misc'
10 -import { Btn, Flex, IconBtn, iconTooltip, reloadBtn, useBreakpoint, useToggleButton } from './mui'
10 +import { Btn, execDoneMessage, Flex, IconBtn, iconTooltip, reloadBtn, useBreakpoint, useToggleButton } from './mui'
11 import { TreeItem, SimpleTreeView } from '@mui/x-tree-view'
12 import MenuButton from './MenuButton'
13 import AccountForm from './AccountForm'
@@ -58,10 +58,10 @@ export default function AccountsPage() {
58 h(IconBtn, { icon: Close, title: "Close", onClick: selectNone }),
59 ],
60 reload,
61 - done(username) {
61 + done(username, saveBtn) {
62 setSel(isSideBreakpoint ? [username] : [])
63 reload()
64 - toast("Account saved", 'success')
64 + execDoneMessage('', saveBtn)
65 }
66 }))
67 useEffect(() => {
mui-grid-form/index.ts
+1
@@ -227,6 +227,7 @@ export function Form<Values extends Dict>({
227 children: "Save",
228 loading: useDebounce(phase !== Phase.Idle), // debounce fixes click being ignored at state change and flickering
229 ...saveBtn,
230 + className: `saveBtn ${saveBtn?.className||''}`,
231 onClick() {
232 pleaseSubmitAndValidate()
233 },