Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion front_admin/src/lib/api/users.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,31 @@ type AdminUserResponse = {
user: AdminUser;
};

type AdminUsersListParams = {
limit?: number;
offset?: number;
search?: string;
role?: AdminUser["role"];
isBlocked?: boolean;
};

function buildUsersQuery(params?: AdminUsersListParams) {
if (!params) return "";

const query = new URLSearchParams();

Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === "") return;
query.set(key, String(value));
});

const queryString = query.toString();
return queryString ? `?${queryString}` : "";
}

export const adminUsersApi = {
list: () => api.get<AdminUsersListResponse>("/admin/users"),
list: (params?: AdminUsersListParams) =>
api.get<AdminUsersListResponse>(`/admin/users${buildUsersQuery(params)}`),
block: (id: string) => api.patch<AdminUserResponse>(`/admin/users/${id}/block`),
unblock: (id: string) =>
api.patch<AdminUserResponse>(`/admin/users/${id}/unblock`),
Expand Down
28 changes: 25 additions & 3 deletions front_admin/src/modules/users/UsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type RoleFilter = "all" | BackendUserRole;
type StatusFilter = "all" | "active" | "blocked";

const USERS_PER_PAGE = 10;
const USERS_FETCH_LIMIT = 100;
const ROLE_VALUES: BackendUserRole[] = ["super_admin", "admin", "support", "user"];

const ROLE_SUMMARY: Record<
Expand Down Expand Up @@ -66,6 +67,28 @@ function mapUser(user: BackendAdminUser): AdminUser {
};
}

async function fetchAllUsers() {
const firstPage = await adminUsersApi.list({
limit: USERS_FETCH_LIMIT,
offset: 0,
});
const allUsers = [...firstPage.data];

for (
let offset = firstPage.offset + firstPage.limit;
offset < firstPage.total;
offset += firstPage.limit
) {
const page = await adminUsersApi.list({
limit: USERS_FETCH_LIMIT,
offset,
});
allUsers.push(...page.data);
}

return allUsers;
}

function StatCard({
label,
value,
Expand Down Expand Up @@ -123,11 +146,10 @@ export function UsersPage() {
useEffect(() => {
let active = true;

adminUsersApi
.list()
fetchAllUsers()
.then((result) => {
if (!active) return;
setUsers(result.data.map(mapUser));
setUsers(result.map(mapUser));
setError(null);
})
.catch(() => {
Expand Down
7 changes: 6 additions & 1 deletion front_admin/tests/lib/api/resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,17 @@ describe("api resources", () => {

it("calls admin user mutation endpoints", () => {
adminUsersApi.list();
adminUsersApi.list({ limit: 100, offset: 200 });
adminUsersApi.block("u1");
adminUsersApi.unblock("u1");
adminUsersApi.changeRole("u1", "support");
adminUsersApi.delete("u1");

expect(mockedApi.get).toHaveBeenCalledWith("/admin/users");
expect(mockedApi.get).toHaveBeenNthCalledWith(1, "/admin/users");
expect(mockedApi.get).toHaveBeenNthCalledWith(
2,
"/admin/users?limit=100&offset=200",
);
expect(mockedApi.patch).toHaveBeenNthCalledWith(
1,
"/admin/users/u1/block",
Expand Down
39 changes: 39 additions & 0 deletions front_admin/tests/modules/users/UsersPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ const manyUsers = Array.from({ length: 12 }, (_, index) => ({
isBlocked: index % 3 === 0,
}));

const paginatedUsers = Array.from({ length: 105 }, (_, index) => ({
...backendUsers[index % 2],
id: `00000000-0000-4000-8000-${String(index + 100).padStart(12, "0")}`,
displayName: `Paged User ${index + 1}`,
username: `paged${index + 1}`,
email: `paged${index + 1}@example.com`,
role: index % 2 === 0 ? ("admin" as const) : ("user" as const),
isBlocked: false,
}));

describe("UsersPage", () => {
beforeEach(() => {
vi.clearAllMocks();
Expand Down Expand Up @@ -164,6 +174,35 @@ describe("UsersPage", () => {
expect(screen.getByText(/Exibindo 1-/)).toBeInTheDocument();
});

it("loads every backend page before paginating locally", async () => {
vi.mocked(adminUsersApi.list).mockImplementation(({ offset = 0 } = {}) =>
Promise.resolve({
data: paginatedUsers.slice(offset, offset + 100),
total: paginatedUsers.length,
limit: 100,
offset,
}),
);

renderWithProviders(<UsersPage />);

await screen.findByText("Paged User 1");

await waitFor(() =>
expect(adminUsersApi.list).toHaveBeenCalledWith({
limit: 100,
offset: 100,
}),
);

for (let page = 1; page < 11; page += 1) {
fireEvent.click(screen.getByRole("button", { name: "Próxima página" }));
}

expect(screen.getByText("Paged User 101")).toBeInTheDocument();
expect(screen.getByText("Exibindo 101-105 de 105 usuários")).toBeInTheDocument();
});

it("unblocks a selected blocked user without changing role", async () => {
renderWithProviders(<UsersPage />);

Expand Down
Loading