mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
4dc46e176a
* 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.
154 lines
5.2 KiB
TypeScript
154 lines
5.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { RotateCcw, UserPlus, Users } from 'lucide-react';
|
|
import type { NdUser } from '../../api/navidromeAdmin';
|
|
import ConfirmModal from '../ConfirmModal';
|
|
import { useUserMgmtData } from '../../hooks/useUserMgmtData';
|
|
import { useUserMgmtActions } from '../../hooks/useUserMgmtActions';
|
|
import { UserForm } from './UserForm';
|
|
import { UserMgmtRow } from './userMgmt/UserMgmtRow';
|
|
import { MagicStringModal } from './userMgmt/MagicStringModal';
|
|
|
|
export function UserManagementSection({
|
|
serverUrl,
|
|
token,
|
|
currentUsername,
|
|
}: {
|
|
serverUrl: string;
|
|
token: string;
|
|
currentUsername: string;
|
|
}) {
|
|
const { t, i18n } = useTranslation();
|
|
const { users, libraries, loading, loadError, load } = useUserMgmtData(serverUrl, token, t);
|
|
const [editing, setEditing] = useState<NdUser | 'new' | null>(null);
|
|
const [confirmingDelete, setConfirmingDelete] = useState<NdUser | null>(null);
|
|
const [magicRowUser, setMagicRowUser] = useState<NdUser | null>(null);
|
|
const { busy, handleSave, handleSaveAndGetMagic, performDelete } = useUserMgmtActions({
|
|
serverUrl, token, libraries, editing, setEditing, reload: load, t,
|
|
});
|
|
|
|
return (
|
|
<section className="settings-section">
|
|
<div className="settings-section-header">
|
|
<Users size={18} />
|
|
<h2>{t('settings.userMgmtTitle')}</h2>
|
|
</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
|
|
{t('settings.userMgmtDesc')}
|
|
</div>
|
|
|
|
{loading && (
|
|
<div className="settings-card" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<div className="spinner" style={{ width: 14, height: 14 }} />
|
|
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>…</span>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && loadError && (
|
|
<div
|
|
className="settings-card"
|
|
style={{
|
|
color: 'var(--danger)',
|
|
fontSize: 13,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
gap: 12,
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
<div style={{ flex: 1, minWidth: 200 }}>
|
|
<div style={{ fontWeight: 600, marginBottom: 4 }}>{t('settings.userMgmtLoadFriendly')}</div>
|
|
<div style={{ fontSize: 11, color: 'var(--text-muted)', wordBreak: 'break-word' }}>{loadError}</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary"
|
|
onClick={() => void load()}
|
|
style={{ flexShrink: 0 }}
|
|
>
|
|
<RotateCcw size={14} /> {t('settings.userMgmtRetry')}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !loadError && (
|
|
<>
|
|
{editing ? (
|
|
<UserForm
|
|
initial={editing === 'new' ? null : editing}
|
|
libraries={libraries}
|
|
shareServerUrl={serverUrl}
|
|
ndToken={token}
|
|
onUsersDirty={load}
|
|
onSave={handleSave}
|
|
onSaveAndGetMagic={editing === 'new' ? handleSaveAndGetMagic : undefined}
|
|
onCancel={() => setEditing(null)}
|
|
busy={busy}
|
|
/>
|
|
) : (
|
|
<button
|
|
className="btn btn-surface"
|
|
style={{ marginBottom: '0.75rem' }}
|
|
onClick={() => setEditing('new')}
|
|
disabled={busy}
|
|
>
|
|
<UserPlus size={16} /> {t('settings.userMgmtAddUser')}
|
|
</button>
|
|
)}
|
|
|
|
{users.length === 0 ? (
|
|
<div className="settings-card" style={{ color: 'var(--text-muted)', fontSize: 14 }}>
|
|
{t('settings.userMgmtEmpty')}
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
{users.map(u => (
|
|
<UserMgmtRow
|
|
key={u.id}
|
|
user={u}
|
|
libraries={libraries}
|
|
isSelf={u.userName === currentUsername}
|
|
busy={busy}
|
|
onEdit={setEditing}
|
|
onRequestDelete={setConfirmingDelete}
|
|
onRequestMagic={setMagicRowUser}
|
|
t={t}
|
|
i18n={i18n}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
<ConfirmModal
|
|
open={!!confirmingDelete}
|
|
title={t('settings.userMgmtDelete')}
|
|
message={confirmingDelete
|
|
? t('settings.userMgmtConfirmDelete', { username: confirmingDelete.userName })
|
|
: ''}
|
|
confirmLabel={t('settings.userMgmtDelete')}
|
|
cancelLabel={t('settings.userMgmtCancel')}
|
|
danger
|
|
onConfirm={() => {
|
|
if (!confirmingDelete) return;
|
|
const target = confirmingDelete;
|
|
setConfirmingDelete(null);
|
|
void performDelete(target);
|
|
}}
|
|
onCancel={() => setConfirmingDelete(null)}
|
|
/>
|
|
{magicRowUser && (
|
|
<MagicStringModal
|
|
user={magicRowUser}
|
|
serverUrl={serverUrl}
|
|
token={token}
|
|
onClose={() => setMagicRowUser(null)}
|
|
onSuccess={load}
|
|
t={t}
|
|
/>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|