feat(web): add user edit dialog to admin users page (MS21-UI-002) #578
@@ -9,7 +9,7 @@ import {
|
|||||||
type SyntheticEvent,
|
type SyntheticEvent,
|
||||||
} from "react";
|
} from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { UserPlus, UserX } from "lucide-react";
|
import { Pencil, UserPlus, UserX } from "lucide-react";
|
||||||
import { WorkspaceMemberRole } from "@mosaic/shared";
|
import { WorkspaceMemberRole } from "@mosaic/shared";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -46,9 +46,11 @@ import {
|
|||||||
deactivateUser,
|
deactivateUser,
|
||||||
fetchAdminUsers,
|
fetchAdminUsers,
|
||||||
inviteUser,
|
inviteUser,
|
||||||
|
updateUser,
|
||||||
type AdminUser,
|
type AdminUser,
|
||||||
type AdminUsersResponse,
|
type AdminUsersResponse,
|
||||||
type InviteUserDto,
|
type InviteUserDto,
|
||||||
|
type UpdateUserDto,
|
||||||
} from "@/lib/api/admin";
|
} from "@/lib/api/admin";
|
||||||
|
|
||||||
const ROLE_PRIORITY: Record<WorkspaceMemberRole, number> = {
|
const ROLE_PRIORITY: Record<WorkspaceMemberRole, number> = {
|
||||||
@@ -98,6 +100,11 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
const [deactivateTarget, setDeactivateTarget] = useState<AdminUser | null>(null);
|
const [deactivateTarget, setDeactivateTarget] = useState<AdminUser | null>(null);
|
||||||
const [isDeactivating, setIsDeactivating] = useState<boolean>(false);
|
const [isDeactivating, setIsDeactivating] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [editTarget, setEditTarget] = useState<AdminUser | null>(null);
|
||||||
|
const [editName, setEditName] = useState<string>("");
|
||||||
|
const [editError, setEditError] = useState<string | null>(null);
|
||||||
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
|
|
||||||
const loadUsers = useCallback(async (showLoadingState: boolean): Promise<void> => {
|
const loadUsers = useCallback(async (showLoadingState: boolean): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (showLoadingState) {
|
if (showLoadingState) {
|
||||||
@@ -188,6 +195,23 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleEditSubmit(): Promise<void> {
|
||||||
|
if (editTarget === null) return;
|
||||||
|
setIsEditing(true);
|
||||||
|
setEditError(null);
|
||||||
|
try {
|
||||||
|
const dto: UpdateUserDto = {};
|
||||||
|
if (editName.trim()) dto.name = editName.trim();
|
||||||
|
await updateUser(editTarget.id, dto);
|
||||||
|
setEditTarget(null);
|
||||||
|
await loadUsers(false);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setEditError(err instanceof Error ? err.message : "Failed to update user");
|
||||||
|
} finally {
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto p-6 space-y-6">
|
<div className="max-w-6xl mx-auto p-6 space-y-6">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
@@ -381,6 +405,18 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
<Badge variant={isActive ? "secondary" : "destructive"}>
|
<Badge variant={isActive ? "secondary" : "destructive"}>
|
||||||
{isActive ? "Active" : "Inactive"}
|
{isActive ? "Active" : "Inactive"}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setEditTarget(user);
|
||||||
|
setEditName(user.name);
|
||||||
|
setEditError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4 mr-2" />
|
||||||
|
Edit Role
|
||||||
|
</Button>
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@@ -432,4 +468,55 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={editTarget !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open && !isEditing) {
|
||||||
|
setEditTarget(null);
|
||||||
|
setEditError(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit User Role</DialogTitle>
|
||||||
|
<DialogDescription>Change role for {editTarget?.email ?? "user"}.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
{editError !== null ? <p className="text-sm text-destructive">{editError}</p> : null}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-name">Display Name</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-name"
|
||||||
|
value={editName}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setEditName(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Full name"
|
||||||
|
disabled={isEditing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setEditTarget(null);
|
||||||
|
}}
|
||||||
|
disabled={isEditing}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
void handleEditSubmit();
|
||||||
|
}}
|
||||||
|
disabled={isEditing}
|
||||||
|
>
|
||||||
|
{isEditing ? "Saving..." : "Save"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user