feat: admin can change user display name
API: new updateName adminProcedure in user router - Input: userId + name (min 1, max 200 chars) - Argon2 not involved (name only, not password) - Audit log: "Changed name from X to Y" UI: "Display Name" editable section in user edit modal - Shows current name with "Edit" link - Click Edit: inline input with Save/Cancel + Enter/Escape - Auto-focuses input, saves on Enter - Invalidates user list on success Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -92,6 +92,7 @@ export function UsersClient() {
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [roleFilter, setRoleFilter] = useState<SystemRole | "">("");
|
||||
const [editingName, setEditingName] = useState<{ userId: string; name: string } | null>(null);
|
||||
const [passwordTarget, setPasswordTarget] = useState<{ userId: string; userName: string } | null>(null);
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
@@ -187,6 +188,14 @@ export function UsersClient() {
|
||||
onError: (err) => setPasswordError(err.message),
|
||||
});
|
||||
|
||||
const updateNameMutation = trpc.user.updateName.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.user.list.invalidate();
|
||||
setEditingName(null);
|
||||
},
|
||||
onError: (err) => setActionError(err.message),
|
||||
});
|
||||
|
||||
function openSetPassword(user: UserRow) {
|
||||
setPasswordTarget({ userId: user.id, userName: user.name ?? user.email });
|
||||
setNewPassword("");
|
||||
@@ -756,6 +765,61 @@ export function UsersClient() {
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="overflow-y-auto flex-1 px-6 py-5 space-y-6">
|
||||
{/* User Name */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||||
Display Name
|
||||
</h3>
|
||||
{editingName?.userId === editState.userId ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editingName.name}
|
||||
onChange={(e) => setEditingName({ ...editingName, name: e.target.value })}
|
||||
className="flex-1 border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && editingName.name.trim()) {
|
||||
updateNameMutation.mutate({ id: editingName.userId, name: editingName.name.trim() });
|
||||
}
|
||||
if (e.key === "Escape") setEditingName(null);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateNameMutation.mutate({ id: editingName.userId, name: editingName.name.trim() })}
|
||||
disabled={!editingName.name.trim() || updateNameMutation.isPending}
|
||||
className="px-3 py-2 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 disabled:opacity-50"
|
||||
>
|
||||
{updateNameMutation.isPending ? "..." : "Save"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingName(null)}
|
||||
className="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||
{(users as any)?.find((u: any) => u.id === editState.userId)?.name ?? "—"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const user = (users as any)?.find((u: any) => u.id === editState.userId);
|
||||
setEditingName({ userId: editState.userId, name: user?.name ?? "" });
|
||||
}}
|
||||
className="text-xs text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* System Role */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center">
|
||||
|
||||
Reference in New Issue
Block a user