fix(web): QA fixes on users settings page (MS21-UI-001-QA) (#599)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful

fix(web): QA fixes on users settings page (MS21-UI-001-QA)

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #599.
This commit is contained in:
2026-03-01 13:52:15 +00:00
committed by jason.woltje
parent 8ea3c3ee67
commit 512a29a240
2 changed files with 296 additions and 64 deletions

View File

@@ -55,6 +55,7 @@ import {
type InviteUserDto,
type UpdateUserDto,
} from "@/lib/api/admin";
import { useAuth } from "@/lib/auth/auth-context";
import { fetchUserWorkspaces, updateWorkspaceMemberRole } from "@/lib/api/workspaces";
import { SettingsAccessDenied } from "@/components/settings/SettingsAccessDenied";
@@ -77,6 +78,7 @@ const INITIAL_DETAIL_FORM = {
workspaceId: null as string | null,
workspaceName: null as string | null,
};
const USERS_PAGE_SIZE = 50;
interface DetailInitialState {
name: string;
@@ -104,8 +106,11 @@ function getPrimaryMembership(user: AdminUser): AdminWorkspaceMembership | null
}
export default function UsersSettingsPage(): ReactElement {
const { user: authUser } = useAuth();
const [users, setUsers] = useState<AdminUser[]>([]);
const [meta, setMeta] = useState<AdminUsersResponse["meta"] | null>(null);
const [page, setPage] = useState<number>(1);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
@@ -127,25 +132,35 @@ export default function UsersSettingsPage(): ReactElement {
const [deactivateTarget, setDeactivateTarget] = useState<AdminUser | null>(null);
const [isDeactivating, setIsDeactivating] = useState<boolean>(false);
const loadUsers = useCallback(async (showLoadingState: boolean): Promise<void> => {
try {
if (showLoadingState) {
setIsLoading(true);
} else {
setIsRefreshing(true);
}
const loadUsers = useCallback(
async (showLoadingState: boolean): Promise<void> => {
try {
if (showLoadingState) {
setIsLoading(true);
} else {
setIsRefreshing(true);
}
const response = await fetchAdminUsers(1, 50);
setUsers(response.data);
setMeta(response.meta);
setError(null);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to load admin users");
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
}, []);
const response = await fetchAdminUsers(page, USERS_PAGE_SIZE);
const lastValidPage = Math.max(1, response.meta.totalPages);
if (page > lastValidPage) {
setPage(lastValidPage);
return;
}
setUsers(response.data);
setMeta(response.meta);
setError(null);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to load admin users");
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
},
[page]
);
useEffect(() => {
fetchUserWorkspaces()
@@ -170,7 +185,7 @@ export default function UsersSettingsPage(): ReactElement {
}
void loadUsers(true);
}, [isAdmin, loadUsers]);
}, [isAdmin, loadUsers, page]);
function resetInviteForm(): void {
setInviteForm(INITIAL_INVITE_FORM);
@@ -324,6 +339,12 @@ export default function UsersSettingsPage(): ReactElement {
return;
}
if (authUser?.id === deactivateTarget.id) {
setDeactivateTarget(null);
setError("You cannot deactivate your own account.");
return;
}
try {
setIsDeactivating(true);
await deactivateUser(deactivateTarget.id);
@@ -481,7 +502,13 @@ export default function UsersSettingsPage(): ReactElement {
</Link>
</div>
{error ? (
{isLoading ? (
<Card>
<CardContent className="py-12 text-center text-muted-foreground">
Loading users...
</CardContent>
</Card>
) : error ? (
<Card>
<CardContent className="py-4">
<p className="text-sm text-destructive" role="alert">
@@ -489,14 +516,6 @@ export default function UsersSettingsPage(): ReactElement {
</p>
</CardContent>
</Card>
) : null}
{isLoading ? (
<Card>
<CardContent className="py-12 text-center text-muted-foreground">
Loading users...
</CardContent>
</Card>
) : users.length === 0 ? (
<Card>
<CardHeader>
@@ -514,6 +533,7 @@ export default function UsersSettingsPage(): ReactElement {
{users.map((user) => {
const primaryMembership = getPrimaryMembership(user);
const isActive = user.deactivatedAt === null;
const isCurrentUser = authUser?.id === user.id;
return (
<div
@@ -529,7 +549,14 @@ export default function UsersSettingsPage(): ReactElement {
}}
>
<div className="space-y-1 min-w-0">
<p className="font-semibold truncate">{user.name || "Unnamed User"}</p>
<p className="font-semibold truncate">
{user.name || "Unnamed User"}
{isCurrentUser ? (
<span className="ml-2 text-xs font-normal text-muted-foreground">
(You)
</span>
) : null}
</p>
<p className="text-sm text-muted-foreground truncate">{user.email}</p>
</div>
@@ -540,7 +567,7 @@ export default function UsersSettingsPage(): ReactElement {
<Badge variant={isActive ? "secondary" : "destructive"}>
{isActive ? "Active" : "Inactive"}
</Badge>
{isActive ? (
{isActive && !isCurrentUser ? (
<Button
variant="destructive"
size="sm"
@@ -557,6 +584,36 @@ export default function UsersSettingsPage(): ReactElement {
</div>
);
})}
{meta && meta.totalPages > 1 ? (
<div className="flex items-center justify-between pt-3 mt-1 border-t">
<p className="text-sm text-muted-foreground">
Page {page} of {meta.totalPages}
</p>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={page === 1}
onClick={() => {
setPage((previousPage) => Math.max(1, previousPage - 1));
}}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
disabled={page >= meta.totalPages}
onClick={() => {
setPage((previousPage) => Math.min(meta.totalPages, previousPage + 1));
}}
>
Next
</Button>
</div>
</div>
) : null}
</CardContent>
</Card>
)}