fix(ui): подтверждать удаление пользователя и сохранять ошибку

This commit is contained in:
Denozordec committed 2026-10-01 09:39:39 +07:00
1 parent 2d5fb3598f
commit 02ca48ef9c
8 files changed
+92 -23

No files matched your search

+38 -17
View File
@@ -84,6 +84,13 @@ function UsersContent() {
const loadController = useRef<AbortController | null>(null)
const loadRevision = useRef(0)
const [saving, setSaving] = useState(false)
const [deleting, setDeleting] = useState(false)
const [deleteError, setDeleteError] = useState<string | null>(null)
const [operationNotice, setOperationNotice] = useState<string | null>(null)
const mutationLock = useRef(false)
const mutationController = useRef<AbortController | null>(null)
const alive = useRef(true)
useEffect(() => { alive.current = true; return () => { alive.current = false; mutationController.current?.abort() } }, [])
const [sheetOpen, setSheetOpen] = useState(false)
const [editUser, setEditUser] = useState<AppUser | null>(null)
const [deleteTarget, setDeleteTarget] = useState<AppUser | null>(null)
@@ -220,20 +227,32 @@ function UsersContent() {
}
const handleDelete = async () => {
if (!deleteTarget || !writeReady) return
if (!deleteTarget || !writeReady || mutationLock.current) return
const target = deleteTarget
if (!isLive) {
setUsers((prev) => prev.filter((u) => u.id !== deleteTarget.id))
setDeleteTarget(null)
toast.success("Пользователь удалён")
setUsers(prev => prev.filter(u => u.id !== target.id)); setDeleteTarget(null); setDeleteError(null)
toast.success("Пользователь удалён в демо.")
return
}
mutationLock.current = true
const controller = new AbortController()
mutationController.current = controller
setDeleting(true); setDeleteError(null); setOperationNotice(null)
const current = () => alive.current && !controller.signal.aborted
try {
await deleteAppUser(backendUrl, deleteTarget.id)
await deleteAppUser(backendUrl, target.id, { signal: AbortSignal.any([controller.signal, AbortSignal.timeout(30_000)]) })
if (!current()) return
setUsers(prev => prev.filter(u => u.id !== target.id))
setDeleteTarget(null)
await loadLive()
toast.success("Пользователь удалён")
} catch (err) {
toast.error(err instanceof Error ? err.message : "Не удалось удалить")
setOperationNotice("Удаление пользователя подтверждено API.")
toast.success("Пользователь удалён.")
const refreshed = await loadLive()
if (current() && !refreshed) setOperationNotice("Удаление пользователя подтверждено API. Обновление каталогов не удалось; повторите чтение.")
} catch (error) {
if (current()) setDeleteError(error instanceof Error ? error.message : "Удаление не подтверждено.")
} finally {
if (mutationController.current === controller) { mutationController.current = null; mutationLock.current = false }
if (alive.current) setDeleting(false)
}
}
@@ -243,8 +262,8 @@ function UsersContent() {
crumbs={[{ label: "Управление" }, { label: "Пользователи" }]}
actions={
<>
{isLive && <Button type="button" size="sm" variant="outline" disabled={!canRead || loading || saving || sheetOpen} onClick={() => void loadLive()}>Обновить пользователей</Button>}
<Button type="button" size="sm" disabled={!writeReady} onClick={() => { setEditUser(null); setSheetOpen(true) }}>
{isLive && <Button type="button" size="sm" variant="outline" disabled={!canRead || loading || saving || deleting || sheetOpen || !!deleteTarget} onClick={() => void loadLive()}>Обновить пользователей</Button>}
<Button type="button" size="sm" disabled={!writeReady || saving || deleting} onClick={() => { setEditUser(null); setSheetOpen(true) }}>
<PlusIcon className="size-4" />Добавить пользователя
</Button>
</>
@@ -252,6 +271,7 @@ function UsersContent() {
/>
<div className="flex-1 overflow-y-auto p-6 flex flex-col gap-5">
{operationNotice && <p role="status" className="text-sm text-muted-foreground">{operationNotice}</p>}
{!isLive && <p role="status" className="text-sm text-muted-foreground">Демо: изменения сохраняются только в текущем экране.</p>}
{!canRead && <p role="status">Для чтения пользователей требуется mm:users:read.</p>}
{!canWrite && <p role="status">Для изменения пользователей требуется mm:users:write.</p>}
@@ -295,9 +315,9 @@ function UsersContent() {
serversCount={serversKnown ? serverOptions.length : null}
allSectionsCount={ALL_SECTIONS.length}
isLoading={loading && !usersKnown}
actionsDisabled={!writeReady || saving}
actionsDisabled={!writeReady || saving || deleting}
onEdit={(u) => { setEditUser(u); setSheetOpen(true) }}
onDelete={setDeleteTarget}
onDelete={u => { setDeleteError(null); setDeleteTarget(u) }}
/>
</DataPageCard>}
</div>
@@ -316,7 +336,7 @@ function UsersContent() {
/>
{deleteTarget && (
<AlertDialog open={!!deleteTarget} onOpenChange={(v) => { if (!v) setDeleteTarget(null) }}>
<AlertDialog open={!!deleteTarget} onOpenChange={(v) => { if (!v && !deleting) { setDeleteTarget(null); setDeleteError(null) } }}>
<AlertDialogContent size="default">
<AlertDialogHeader>
<AlertDialogMedia className="bg-destructive/10 text-destructive">
@@ -328,9 +348,10 @@ function UsersContent() {
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setDeleteTarget(null)}>Отмена</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={() => { void handleDelete() }}>
Удалить
{deleteError && <p role="alert" className="text-sm text-destructive">{deleteError}</p>}
<AlertDialogCancel disabled={deleting} onClick={() => { setDeleteTarget(null); setDeleteError(null) }}>Отмена</AlertDialogCancel>
<AlertDialogAction type="button" disabled={deleting || !writeReady} aria-busy={deleting} variant="destructive" onClick={() => { void handleDelete() }}>
{deleting ? "Удаление…" : "Удалить"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>