Files
psysonic/src/hooks/useUserMgmtData.ts
T
Frank Stellmacher 4dc46e176a refactor(user-mgmt): I.5 — split UserManagementSection.tsx 515 → 153 LOC across 6 files (#677)
* refactor(user-mgmt): extract formatLastSeen helper

Move the relative-time formatter (with the Navidrome
'0001-01-01T00:00:00Z' epoch guard) into utils/userMgmtHelpers.ts.

UserManagementSection.tsx: 515 → 499 LOC.

* refactor(user-mgmt): extract useUserMgmtData hook

Pull users + libraries state, sequential admin-API fetch, and the
nginx-friendly error normalisation into hooks/useUserMgmtData.ts.

UserManagementSection.tsx: 499 → 464 LOC.

* refactor(user-mgmt): extract useUserMgmtActions hook

Bundle handleSave (covers create + edit + library assignment),
handleSaveAndGetMagic (new non-admin user → encoded magic string on
clipboard), and performDelete into hooks/useUserMgmtActions.ts. The
delete confirmation modal now closes inline in the parent before
delegating to performDelete so the hook stays agnostic of UI state.

UserManagementSection.tsx: 464 → 343 LOC.

* refactor(user-mgmt): extract UserMgmtRow subcomponent

Move the per-user list row (user/admin badges, lib-names blob, magic-
string + delete actions, keyboard activation) into
components/settings/userMgmt/UserMgmtRow.tsx.

UserManagementSection.tsx: 343 → 272 LOC.

* refactor(user-mgmt): extract MagicStringModal subcomponent

Move the per-user magic-string portal modal (password re-set + clipboard
copy of the encoded server-magic-string) into
components/settings/userMgmt/MagicStringModal.tsx. Internal password and
submitting state move into the modal; the parent only owns which user is
targeted.

UserManagementSection.tsx: 272 → 153 LOC.
2026-05-14 00:45:48 +02:00

63 lines
2.1 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import type { TFunction } from 'i18next';
import {
ndListLibraries,
ndListUsers,
type NdLibrary,
type NdUser,
} from '../api/navidromeAdmin';
interface UseUserMgmtDataResult {
users: NdUser[];
libraries: NdLibrary[];
loading: boolean;
loadError: string | null;
load: () => Promise<void>;
}
/**
* Fetch and keep the Navidrome admin users + libraries lists in sync.
*
* The two requests are **sequential, not parallel**: some nginx setups
* with churning upstream keep-alive drop one of two parallel TLS
* connections. Doing users first then libraries keeps us on one
* connection at a time and pairs cleanly with the `nd_retry` backoff on
* the Rust side.
*
* Tauri's `invoke` rejects with a bare `string` (our Rust commands
* return `Err(String)`), so the error normalisation surfaces the real
* cause (e.g. `tls handshake eof`) instead of falling back to the
* generic i18n string.
*/
export function useUserMgmtData(serverUrl: string, token: string, t: TFunction): UseUserMgmtDataResult {
const [users, setUsers] = useState<NdUser[]>([]);
const [libraries, setLibraries] = useState<NdLibrary[]>([]);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setLoadError(null);
try {
const list = await ndListUsers(serverUrl, token);
const libs = await ndListLibraries(serverUrl, token).catch(() => [] as NdLibrary[]);
setUsers([...list].sort((a, b) => a.userName.localeCompare(b.userName)));
setLibraries([...libs].sort((a, b) => a.name.localeCompare(b.name)));
} catch (e) {
const raw = typeof e === 'string'
? e
: (e instanceof Error && e.message)
? e.message
: '';
const prefix = t('settings.userMgmtLoadError');
setLoadError(raw ? `${prefix} ${raw}` : prefix);
} finally {
setLoading(false);
}
}, [serverUrl, token, t]);
useEffect(() => { void load(); }, [load]);
return { users, libraries, loading, loadError, load };
}