Compare commits
1 Commits
fix/csrf-b
...
fix/ms21-u
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e0eb53cf1 |
@@ -1,17 +1,19 @@
|
|||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
|
|
||||||
import { WorkspaceMemberRole } from "@mosaic/shared";
|
import { WorkspaceMemberRole } from "@mosaic/shared";
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type AdminUser,
|
||||||
deactivateUser,
|
deactivateUser,
|
||||||
fetchAdminUsers,
|
fetchAdminUsers,
|
||||||
inviteUser,
|
inviteUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
type AdminUsersResponse,
|
type AdminUsersResponse,
|
||||||
} from "@/lib/api/admin";
|
} from "@/lib/api/admin";
|
||||||
|
import { useAuth } from "@/lib/auth/auth-context";
|
||||||
import { fetchUserWorkspaces, updateWorkspaceMemberRole } from "@/lib/api/workspaces";
|
import { fetchUserWorkspaces, updateWorkspaceMemberRole } from "@/lib/api/workspaces";
|
||||||
import UsersSettingsPage from "./page";
|
import UsersSettingsPage from "./page";
|
||||||
|
|
||||||
@@ -39,48 +41,80 @@ vi.mock("@/lib/api/workspaces", () => ({
|
|||||||
updateWorkspaceMemberRole: vi.fn(),
|
updateWorkspaceMemberRole: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/auth/auth-context", () => ({
|
||||||
|
useAuth: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const fetchAdminUsersMock = vi.mocked(fetchAdminUsers);
|
const fetchAdminUsersMock = vi.mocked(fetchAdminUsers);
|
||||||
const inviteUserMock = vi.mocked(inviteUser);
|
const inviteUserMock = vi.mocked(inviteUser);
|
||||||
const updateUserMock = vi.mocked(updateUser);
|
const updateUserMock = vi.mocked(updateUser);
|
||||||
const deactivateUserMock = vi.mocked(deactivateUser);
|
const deactivateUserMock = vi.mocked(deactivateUser);
|
||||||
const fetchUserWorkspacesMock = vi.mocked(fetchUserWorkspaces);
|
const fetchUserWorkspacesMock = vi.mocked(fetchUserWorkspaces);
|
||||||
const updateWorkspaceMemberRoleMock = vi.mocked(updateWorkspaceMemberRole);
|
const updateWorkspaceMemberRoleMock = vi.mocked(updateWorkspaceMemberRole);
|
||||||
|
const useAuthMock = vi.mocked(useAuth);
|
||||||
|
|
||||||
const adminUsersResponse: AdminUsersResponse = {
|
function makeAdminUser(overrides?: Partial<AdminUser>): AdminUser {
|
||||||
data: [
|
return {
|
||||||
{
|
id: "user-1",
|
||||||
id: "user-1",
|
name: "Alice",
|
||||||
name: "Alice",
|
email: "alice@example.com",
|
||||||
email: "alice@example.com",
|
emailVerified: true,
|
||||||
emailVerified: true,
|
image: null,
|
||||||
image: null,
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
createdAt: "2026-01-01T00:00:00.000Z",
|
deactivatedAt: null,
|
||||||
deactivatedAt: null,
|
isLocalAuth: false,
|
||||||
isLocalAuth: false,
|
invitedAt: null,
|
||||||
invitedAt: null,
|
invitedBy: null,
|
||||||
invitedBy: null,
|
workspaceMemberships: [
|
||||||
workspaceMemberships: [
|
{
|
||||||
{
|
workspaceId: "workspace-1",
|
||||||
workspaceId: "workspace-1",
|
workspaceName: "Personal Workspace",
|
||||||
workspaceName: "Personal Workspace",
|
role: WorkspaceMemberRole.ADMIN,
|
||||||
role: WorkspaceMemberRole.ADMIN,
|
joinedAt: "2026-01-01T00:00:00.000Z",
|
||||||
joinedAt: "2026-01-01T00:00:00.000Z",
|
},
|
||||||
},
|
],
|
||||||
],
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeAdminUsersResponse(options?: {
|
||||||
|
data?: AdminUser[];
|
||||||
|
page?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
total?: number;
|
||||||
|
limit?: number;
|
||||||
|
}): AdminUsersResponse {
|
||||||
|
const data = options?.data ?? [makeAdminUser()];
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
total: options?.total ?? data.length,
|
||||||
|
page: options?.page ?? 1,
|
||||||
|
limit: options?.limit ?? 50,
|
||||||
|
totalPages: options?.totalPages ?? 1,
|
||||||
},
|
},
|
||||||
],
|
};
|
||||||
meta: {
|
}
|
||||||
total: 1,
|
|
||||||
page: 1,
|
function makeAuthState(userId: string): ReturnType<typeof useAuth> {
|
||||||
limit: 50,
|
return {
|
||||||
totalPages: 1,
|
user: { id: userId, email: `${userId}@example.com`, name: "Current User" },
|
||||||
},
|
isLoading: false,
|
||||||
};
|
isAuthenticated: true,
|
||||||
|
authError: null,
|
||||||
|
sessionExpiring: false,
|
||||||
|
sessionMinutesRemaining: 0,
|
||||||
|
signOut: vi.fn(() => Promise.resolve()),
|
||||||
|
refreshSession: vi.fn(() => Promise.resolve()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("UsersSettingsPage", () => {
|
describe("UsersSettingsPage", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
const adminUsersResponse = makeAdminUsersResponse();
|
||||||
|
|
||||||
fetchAdminUsersMock.mockResolvedValue(adminUsersResponse);
|
fetchAdminUsersMock.mockResolvedValue(adminUsersResponse);
|
||||||
fetchUserWorkspacesMock.mockResolvedValue([
|
fetchUserWorkspacesMock.mockResolvedValue([
|
||||||
{
|
{
|
||||||
@@ -97,10 +131,7 @@ describe("UsersSettingsPage", () => {
|
|||||||
email: "new@example.com",
|
email: "new@example.com",
|
||||||
invitedAt: "2026-01-02T00:00:00.000Z",
|
invitedAt: "2026-01-02T00:00:00.000Z",
|
||||||
});
|
});
|
||||||
const firstUser = adminUsersResponse.data[0];
|
const firstUser = adminUsersResponse.data[0] ?? makeAdminUser();
|
||||||
if (!firstUser) {
|
|
||||||
throw new Error("Expected at least one admin user in test fixtures");
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUserMock.mockResolvedValue(firstUser);
|
updateUserMock.mockResolvedValue(firstUser);
|
||||||
deactivateUserMock.mockResolvedValue(firstUser);
|
deactivateUserMock.mockResolvedValue(firstUser);
|
||||||
@@ -116,6 +147,8 @@ describe("UsersSettingsPage", () => {
|
|||||||
image: null,
|
image: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useAuthMock.mockReturnValue(makeAuthState("user-current"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows access denied to non-admin users", async () => {
|
it("shows access denied to non-admin users", async () => {
|
||||||
@@ -174,4 +207,146 @@ describe("UsersSettingsPage", () => {
|
|||||||
|
|
||||||
expect(updateWorkspaceMemberRoleMock).not.toHaveBeenCalled();
|
expect(updateWorkspaceMemberRoleMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("caps pagination to the last valid page after deactivation shrinks the dataset", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const pageOneUser = makeAdminUser({
|
||||||
|
id: "user-1",
|
||||||
|
name: "Alice",
|
||||||
|
email: "alice@example.com",
|
||||||
|
});
|
||||||
|
const pageTwoUser = makeAdminUser({
|
||||||
|
id: "user-2",
|
||||||
|
name: "Bob",
|
||||||
|
email: "bob@example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchAdminUsersMock.mockReset();
|
||||||
|
const responses = [
|
||||||
|
{
|
||||||
|
expectedPage: 1,
|
||||||
|
response: makeAdminUsersResponse({
|
||||||
|
data: [pageOneUser],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 2,
|
||||||
|
total: 2,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expectedPage: 2,
|
||||||
|
response: makeAdminUsersResponse({
|
||||||
|
data: [pageTwoUser],
|
||||||
|
page: 2,
|
||||||
|
totalPages: 2,
|
||||||
|
total: 2,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expectedPage: 2,
|
||||||
|
response: makeAdminUsersResponse({
|
||||||
|
data: [],
|
||||||
|
page: 2,
|
||||||
|
totalPages: 1,
|
||||||
|
total: 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expectedPage: 1,
|
||||||
|
response: makeAdminUsersResponse({
|
||||||
|
data: [pageOneUser],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
total: 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
fetchAdminUsersMock.mockImplementation((page = 1) => {
|
||||||
|
const next = responses.shift();
|
||||||
|
if (!next) {
|
||||||
|
throw new Error("Unexpected fetchAdminUsers call in pagination-cap test");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(page).toBe(next.expectedPage);
|
||||||
|
return Promise.resolve(next.response);
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<UsersSettingsPage />);
|
||||||
|
|
||||||
|
expect(await screen.findByText("alice@example.com")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
expect(await screen.findByText("bob@example.com")).toBeInTheDocument();
|
||||||
|
|
||||||
|
const pageTwoRow = screen.getByText("bob@example.com").closest('[role="button"]');
|
||||||
|
if (!(pageTwoRow instanceof HTMLElement)) {
|
||||||
|
throw new Error("Expected Bob's row to exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.click(within(pageTwoRow).getByRole("button", { name: "Deactivate" }));
|
||||||
|
const deactivateButtons = await screen.findAllByRole("button", { name: "Deactivate" });
|
||||||
|
const confirmDeactivateButton = deactivateButtons[deactivateButtons.length - 1];
|
||||||
|
if (!confirmDeactivateButton) {
|
||||||
|
throw new Error("Expected confirmation deactivate button to be rendered");
|
||||||
|
}
|
||||||
|
await user.click(confirmDeactivateButton);
|
||||||
|
|
||||||
|
expect(await screen.findByText("alice@example.com")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("No Users Yet")).not.toBeInTheDocument();
|
||||||
|
expect(deactivateUserMock).toHaveBeenCalledWith("user-2");
|
||||||
|
const requestedPages = fetchAdminUsersMock.mock.calls.map(([requestedPage]) => requestedPage);
|
||||||
|
expect(requestedPages.slice(-2)).toEqual([2, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the API error state without rendering the empty-state message", async () => {
|
||||||
|
fetchAdminUsersMock.mockRejectedValueOnce(new Error("Unable to load users"));
|
||||||
|
|
||||||
|
render(<UsersSettingsPage />);
|
||||||
|
|
||||||
|
expect(await screen.findByText("Unable to load users")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("No Users Yet")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Invite the first user to get started.")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prevents the current user from deactivating their own account", async () => {
|
||||||
|
useAuthMock.mockReturnValue(makeAuthState("user-1"));
|
||||||
|
|
||||||
|
const selfUser = makeAdminUser({
|
||||||
|
id: "user-1",
|
||||||
|
name: "Alice",
|
||||||
|
email: "alice@example.com",
|
||||||
|
});
|
||||||
|
const otherUser = makeAdminUser({
|
||||||
|
id: "user-2",
|
||||||
|
name: "Bob",
|
||||||
|
email: "bob@example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchAdminUsersMock.mockResolvedValueOnce(
|
||||||
|
makeAdminUsersResponse({
|
||||||
|
data: [selfUser, otherUser],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
total: 2,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<UsersSettingsPage />);
|
||||||
|
|
||||||
|
expect(await screen.findByText("alice@example.com")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("bob@example.com")).toBeInTheDocument();
|
||||||
|
|
||||||
|
const selfRow = screen.getByText("alice@example.com").closest('[role="button"]');
|
||||||
|
if (!(selfRow instanceof HTMLElement)) {
|
||||||
|
throw new Error("Expected current-user row to exist");
|
||||||
|
}
|
||||||
|
expect(within(selfRow).queryByRole("button", { name: "Deactivate" })).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
const otherRow = screen.getByText("bob@example.com").closest('[role="button"]');
|
||||||
|
if (!(otherRow instanceof HTMLElement)) {
|
||||||
|
throw new Error("Expected other-user row to exist");
|
||||||
|
}
|
||||||
|
expect(within(otherRow).getByRole("button", { name: "Deactivate" })).toBeInTheDocument();
|
||||||
|
expect(deactivateUserMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import {
|
|||||||
type InviteUserDto,
|
type InviteUserDto,
|
||||||
type UpdateUserDto,
|
type UpdateUserDto,
|
||||||
} from "@/lib/api/admin";
|
} from "@/lib/api/admin";
|
||||||
|
import { useAuth } from "@/lib/auth/auth-context";
|
||||||
import { fetchUserWorkspaces, updateWorkspaceMemberRole } from "@/lib/api/workspaces";
|
import { fetchUserWorkspaces, updateWorkspaceMemberRole } from "@/lib/api/workspaces";
|
||||||
import { SettingsAccessDenied } from "@/components/settings/SettingsAccessDenied";
|
import { SettingsAccessDenied } from "@/components/settings/SettingsAccessDenied";
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ const INITIAL_DETAIL_FORM = {
|
|||||||
workspaceId: null as string | null,
|
workspaceId: null as string | null,
|
||||||
workspaceName: null as string | null,
|
workspaceName: null as string | null,
|
||||||
};
|
};
|
||||||
|
const USERS_PAGE_SIZE = 50;
|
||||||
|
|
||||||
interface DetailInitialState {
|
interface DetailInitialState {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -104,8 +106,11 @@ function getPrimaryMembership(user: AdminUser): AdminWorkspaceMembership | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function UsersSettingsPage(): ReactElement {
|
export default function UsersSettingsPage(): ReactElement {
|
||||||
|
const { user: authUser } = useAuth();
|
||||||
|
|
||||||
const [users, setUsers] = useState<AdminUser[]>([]);
|
const [users, setUsers] = useState<AdminUser[]>([]);
|
||||||
const [meta, setMeta] = useState<AdminUsersResponse["meta"] | null>(null);
|
const [meta, setMeta] = useState<AdminUsersResponse["meta"] | null>(null);
|
||||||
|
const [page, setPage] = useState<number>(1);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -127,25 +132,35 @@ 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 loadUsers = useCallback(async (showLoadingState: boolean): Promise<void> => {
|
const loadUsers = useCallback(
|
||||||
try {
|
async (showLoadingState: boolean): Promise<void> => {
|
||||||
if (showLoadingState) {
|
try {
|
||||||
setIsLoading(true);
|
if (showLoadingState) {
|
||||||
} else {
|
setIsLoading(true);
|
||||||
setIsRefreshing(true);
|
} else {
|
||||||
}
|
setIsRefreshing(true);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetchAdminUsers(1, 50);
|
const response = await fetchAdminUsers(page, USERS_PAGE_SIZE);
|
||||||
setUsers(response.data);
|
const lastValidPage = Math.max(1, response.meta.totalPages);
|
||||||
setMeta(response.meta);
|
|
||||||
setError(null);
|
if (page > lastValidPage) {
|
||||||
} catch (err: unknown) {
|
setPage(lastValidPage);
|
||||||
setError(err instanceof Error ? err.message : "Failed to load admin users");
|
return;
|
||||||
} finally {
|
}
|
||||||
setIsLoading(false);
|
|
||||||
setIsRefreshing(false);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchUserWorkspaces()
|
fetchUserWorkspaces()
|
||||||
@@ -170,7 +185,7 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loadUsers(true);
|
void loadUsers(true);
|
||||||
}, [isAdmin, loadUsers]);
|
}, [isAdmin, loadUsers, page]);
|
||||||
|
|
||||||
function resetInviteForm(): void {
|
function resetInviteForm(): void {
|
||||||
setInviteForm(INITIAL_INVITE_FORM);
|
setInviteForm(INITIAL_INVITE_FORM);
|
||||||
@@ -324,6 +339,12 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authUser?.id === deactivateTarget.id) {
|
||||||
|
setDeactivateTarget(null);
|
||||||
|
setError("You cannot deactivate your own account.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsDeactivating(true);
|
setIsDeactivating(true);
|
||||||
await deactivateUser(deactivateTarget.id);
|
await deactivateUser(deactivateTarget.id);
|
||||||
@@ -481,7 +502,13 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{isLoading ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-12 text-center text-muted-foreground">
|
||||||
|
Loading users...
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : error ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="py-4">
|
<CardContent className="py-4">
|
||||||
<p className="text-sm text-destructive" role="alert">
|
<p className="text-sm text-destructive" role="alert">
|
||||||
@@ -489,14 +516,6 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isLoading ? (
|
|
||||||
<Card>
|
|
||||||
<CardContent className="py-12 text-center text-muted-foreground">
|
|
||||||
Loading users...
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : users.length === 0 ? (
|
) : users.length === 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -514,6 +533,7 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
{users.map((user) => {
|
{users.map((user) => {
|
||||||
const primaryMembership = getPrimaryMembership(user);
|
const primaryMembership = getPrimaryMembership(user);
|
||||||
const isActive = user.deactivatedAt === null;
|
const isActive = user.deactivatedAt === null;
|
||||||
|
const isCurrentUser = authUser?.id === user.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -529,7 +549,14 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-1 min-w-0">
|
<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>
|
<p className="text-sm text-muted-foreground truncate">{user.email}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -540,7 +567,7 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
<Badge variant={isActive ? "secondary" : "destructive"}>
|
<Badge variant={isActive ? "secondary" : "destructive"}>
|
||||||
{isActive ? "Active" : "Inactive"}
|
{isActive ? "Active" : "Inactive"}
|
||||||
</Badge>
|
</Badge>
|
||||||
{isActive ? (
|
{isActive && !isCurrentUser ? (
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -557,6 +584,36 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user