From 1d0aad6db0ddfe1b6c900313ba17cd13f26804ec Mon Sep 17 00:00:00 2001 From: saumya-cw Date: Fri, 30 Jan 2026 18:22:01 +0530 Subject: [PATCH 1/3] feat: implement AG Grid for user management with header and filters Add header section with title and Show/Hide Filters buttons Configure all user fields as columns with conditional filters Style eyeColor and isActive columns with custom rendering Set responsive layout with violet background and pagination --- package-lock.json | 54 +++++ package.json | 1 + src/App.tsx | 23 +- .../users-grid/components/UsersGrid.test.tsx | 71 +++++++ .../users-grid/components/UsersGrid.tsx | 200 ++++++++++++++++++ .../users-grid/constants/users.constants.ts | 16 ++ src/features/users-grid/hooks/useUsersGrid.ts | 47 ++++ src/features/users-grid/types/users.types.ts | 23 ++ src/index.css | 9 +- vite.config.ts | 4 +- 10 files changed, 427 insertions(+), 21 deletions(-) create mode 100644 src/features/users-grid/components/UsersGrid.test.tsx create mode 100644 src/features/users-grid/components/UsersGrid.tsx create mode 100644 src/features/users-grid/constants/users.constants.ts create mode 100644 src/features/users-grid/hooks/useUsersGrid.ts create mode 100644 src/features/users-grid/types/users.types.ts diff --git a/package-lock.json b/package-lock.json index 8057543..c4ccfae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.18", "ag-grid-community": "^35.0.1", + "ag-grid-enterprise": "^35.0.1", "ag-grid-react": "^35.0.1", "axios": "^1.13.4", "react": "^19.2.0", @@ -2426,6 +2427,46 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/ag-charts-community": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ag-charts-community/-/ag-charts-community-13.0.1.tgz", + "integrity": "sha512-N9DSUcpRFIWjXu1g1za18rZdumI1u1bEBuseL+Rh2ldNk/prKCdZifwo4Tsck0jnVDljyXh/G29nbmzmoXmdHQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ag-charts-core": "13.0.1", + "ag-charts-locale": "13.0.1", + "ag-charts-types": "13.0.1" + } + }, + "node_modules/ag-charts-core": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ag-charts-core/-/ag-charts-core-13.0.1.tgz", + "integrity": "sha512-z6hqKW22nWoIBQJwcd62Z6nMdBE9uc0VLYVAmNdyHbc5qLkcln2s6zGWOo3Y0QIifdDM1dr4ZXcyVjGHudAVtw==", + "license": "MIT", + "optional": true, + "dependencies": { + "ag-charts-types": "13.0.1" + } + }, + "node_modules/ag-charts-enterprise": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ag-charts-enterprise/-/ag-charts-enterprise-13.0.1.tgz", + "integrity": "sha512-70InfMENfHSIoYVrQ9yKbXQCb9uA5qlYOnkYb6yjPFykE6ZSSuOGRI+dFNXQYAbC00wPJKgToX2jXQl5F4+UMA==", + "license": "Commercial", + "optional": true, + "dependencies": { + "ag-charts-community": "13.0.1", + "ag-charts-core": "13.0.1" + } + }, + "node_modules/ag-charts-locale": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ag-charts-locale/-/ag-charts-locale-13.0.1.tgz", + "integrity": "sha512-hr2qFe/rQSJVaRWu+Pj9CfbT5Xb7isHqqrW9eOnXFwkkGZTVM1zJai8DyMcrurC9kbRV2e3cf0toQnHkAzsxdg==", + "license": "MIT", + "optional": true + }, "node_modules/ag-charts-types": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-13.0.1.tgz", @@ -2441,6 +2482,19 @@ "ag-charts-types": "13.0.1" } }, + "node_modules/ag-grid-enterprise": { + "version": "35.0.1", + "resolved": "https://registry.npmjs.org/ag-grid-enterprise/-/ag-grid-enterprise-35.0.1.tgz", + "integrity": "sha512-9mLwf9Bq7+H3EdB6CDO7WqJMQ/YbtpbF+jfX+G8kr9o/m6vgL46E8h7DIEg7gE6QS+IlfY9byLtgoNU8P1PeyQ==", + "license": "Commercial", + "dependencies": { + "ag-grid-community": "35.0.1" + }, + "optionalDependencies": { + "ag-charts-community": "13.0.1", + "ag-charts-enterprise": "13.0.1" + } + }, "node_modules/ag-grid-react": { "version": "35.0.1", "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-35.0.1.tgz", diff --git a/package.json b/package.json index 3ebd843..d27e8e3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.18", "ag-grid-community": "^35.0.1", + "ag-grid-enterprise": "^35.0.1", "ag-grid-react": "^35.0.1", "axios": "^1.13.4", "react": "^19.2.0", diff --git a/src/App.tsx b/src/App.tsx index e73c207..7f1eac4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,27 +1,14 @@ -import './index.css' - +import './index.css'; +import { UsersGrid } from './features/users-grid/components/UsersGrid'; function App() { return ( <> -
-

User Management System

-
-
- -
-
- -
-
- +
+
- ) + ); } export default App diff --git a/src/features/users-grid/components/UsersGrid.test.tsx b/src/features/users-grid/components/UsersGrid.test.tsx new file mode 100644 index 0000000..a1624f9 --- /dev/null +++ b/src/features/users-grid/components/UsersGrid.test.tsx @@ -0,0 +1,71 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; + +import { UsersGrid } from './UsersGrid'; +import type { UserProps } from '../types/users.types'; + +vi.mock('../hooks/useUsersGrid', () => ({ + useUsersApi: () => ({ + users: mockUsers, + isLoading: false, + }), +})); + +const mockUsers: UserProps[] = [ + { + id: 1, + firstName: 'John', + lastName: 'Doe', + maidenName: 'Smith', + age: 30, + gender: 'male', + email: 'john@test.com', + phone: '1234567890', + username: 'johndoe', + birthDate: '1994-01-01', + bloodGroup: 'O+', + height: 180, + weight: 75, + eyeColor: 'Blue', + isActive: true, + }, +]; + + +vi.mock('ag-grid-react', () => ({ + AgGridReact: (props: any) => ( +
+
+ {JSON.stringify(props.rowData)} +
+
+ {JSON.stringify(props.columnDefs)} +
+
+ ), +})); + +describe('UsersGrid', () => { + it('renders AG Grid component', () => { + render(); + + const grid = screen.getByTestId('ag-grid'); + expect(grid).toBeInTheDocument(); + }); + + it('passes row data to AG Grid', () => { + render(); + + const rowData = screen.getByTestId('row-data'); + expect(rowData.textContent).toContain('John'); + }); + + it('passes column definitions to AG Grid', () => { + render(); + + const columnDefs = screen.getByTestId('column-defs'); + expect(columnDefs.textContent).toContain('firstName'); + expect(columnDefs.textContent).toContain('email'); + expect(columnDefs.textContent).toContain('eyeColor'); + }); +}); diff --git a/src/features/users-grid/components/UsersGrid.tsx b/src/features/users-grid/components/UsersGrid.tsx new file mode 100644 index 0000000..3ce9122 --- /dev/null +++ b/src/features/users-grid/components/UsersGrid.tsx @@ -0,0 +1,200 @@ +import { useMemo, useState } from 'react'; +import { AgGridReact } from 'ag-grid-react'; +import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'; +import { SetFilterModule } from 'ag-grid-enterprise'; +import type { ColDef } from 'ag-grid-community'; + +import { useUsersApi } from '../hooks/useUsersGrid'; +import type { UserProps } from '../types/users.types'; +import { DEFAULT_PAGINATION_PAGE_SIZE, PAGINATION_PAGE_SIZE_OPTIONS } from '../constants/users.constants'; + +import 'ag-grid-community/styles/ag-theme-alpine.css'; + +ModuleRegistry.registerModules([AllCommunityModule, SetFilterModule]); + +export function UsersGrid() { + const { users, isLoading } = useUsersApi(); + const [showFilters, setShowFilters] = useState(false); + + const columnDefs = useMemo[]>( + () => [ + { + field: 'id', + headerName: 'ID', + width: 90, + filter: showFilters ? 'agNumberColumnFilter' : false, + }, + { + field: 'firstName', + headerName: 'First Name', + minWidth: 160, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'lastName', + headerName: 'Last Name', + minWidth: 160, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'maidenName', + headerName: 'Maiden Name', + minWidth: 160, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'age', + headerName: 'Age', + width: 100, + filter: showFilters ? 'agNumberColumnFilter' : false, + }, + { + field: 'gender', + headerName: 'Gender', + minWidth: 140, + filter: showFilters ? 'agSetColumnFilter' : false, + }, + { + field: 'email', + headerName: 'Email', + minWidth: 240, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'phone', + headerName: 'Phone', + minWidth: 160, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'username', + headerName: 'Username', + minWidth: 140, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'birthDate', + headerName: 'Birth Date', + minWidth: 140, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'bloodGroup', + headerName: 'Blood Group', + minWidth: 120, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'height', + headerName: 'Height', + width: 100, + filter: showFilters ? 'agNumberColumnFilter' : false, + }, + { + field: 'weight', + headerName: 'Weight', + width: 100, + filter: showFilters ? 'agNumberColumnFilter' : false, + }, + { + field: 'eyeColor', + headerName: 'Eye Color', + minWidth: 120, + filter: showFilters ? 'agTextColumnFilter' : false, + cellStyle: (params) => { + const color = String(params.value).toLowerCase(); + const backgroundMap: Record = { + blue: '#dbeafe', + brown: '#fef3c7', + green: '#dcfce7', + hazel: '#fef9c3', + gray: '#e5e7eb', + black: '#e5e7eb', + }; + return { + backgroundColor: backgroundMap[color] ?? '#f3f4f6', + color: '#111827', + fontWeight: 500, + textTransform: 'capitalize', + }; + }, + }, + { + field: 'isActive', + headerName: 'Active', + width: 120, + filter: showFilters ? 'agTextColumnFilter' : false, + cellRenderer: (params: any) => + params.value ? ( + Active + ) : ( + Inactive + ), + }, + ], + [showFilters] + ); + + const defaultColDef = useMemo( + () => ({ + sortable: true, + resizable: true, + filter: showFilters, + floatingFilter: showFilters, + }), + [showFilters] + ); + + return ( +
+ {/* Header */} +
+

+ 👥 User Management Grid +

+ +
+ + + +
+
+ + {/* Grid Wrapper */} +
+
+ + rowData={users} + columnDefs={columnDefs} + defaultColDef={defaultColDef} + loading={isLoading} + pagination + paginationPageSize={DEFAULT_PAGINATION_PAGE_SIZE} + paginationPageSizeSelector={PAGINATION_PAGE_SIZE_OPTIONS} + /> +
+
+
+ ); +} diff --git a/src/features/users-grid/constants/users.constants.ts b/src/features/users-grid/constants/users.constants.ts new file mode 100644 index 0000000..bca3d4f --- /dev/null +++ b/src/features/users-grid/constants/users.constants.ts @@ -0,0 +1,16 @@ +// User-related constants for the users grid feature + +export const USERS_API_BASE_URL = 'https://dummyjson.com'; + +export const DEFAULT_PAGINATION_PAGE_SIZE = 10; +export const PAGINATION_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; + + +export const Gender = { + Male: 'male', + Female: 'female', +} as const; + +export type Gender = (typeof Gender)[keyof typeof Gender]; + +export const GENDER_OPTIONS = [Gender.Male, Gender.Female]; diff --git a/src/features/users-grid/hooks/useUsersGrid.ts b/src/features/users-grid/hooks/useUsersGrid.ts new file mode 100644 index 0000000..2cbfd64 --- /dev/null +++ b/src/features/users-grid/hooks/useUsersGrid.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import type { UserProps } from '../types/users.types'; +import { USERS_API_BASE_URL } from '../constants/users.constants'; +import axios from 'axios'; + +export function useUsersApi() { + const [users, setUsers] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(function () { + let isMounted = true; + + async function fetchUsersOnMount() { + setIsLoading(true); + + try { + const response = await axios.get<{ users: UserProps[] }>(`${USERS_API_BASE_URL}/users`); + + if (isMounted) { + const mappedUsers = response.data.users.map(function (user: UserProps) { + return { + ...user, + isActive: user.age < 50, + }; + }); + + setUsers(mappedUsers); + } + } finally { + if (isMounted) { + setIsLoading(false); + } + } + } + + fetchUsersOnMount(); + + return function () { + isMounted = false; + }; + }, []); + + return { + users, + isLoading, + }; +} diff --git a/src/features/users-grid/types/users.types.ts b/src/features/users-grid/types/users.types.ts new file mode 100644 index 0000000..dc04c2a --- /dev/null +++ b/src/features/users-grid/types/users.types.ts @@ -0,0 +1,23 @@ +import type { Gender } from '../constants/users.constants'; + +export interface UserProps { + id: number; + firstName: string; + lastName: string; + maidenName: string; + age: number; + gender: Gender; + email: string; + phone: string; + username: string; + birthDate: string; + bloodGroup: string; + height: number; + weight: number; + eyeColor: string; + isActive: boolean; +} + +export interface UsersResponseProps { + users: UserProps[]; +} diff --git a/src/index.css b/src/index.css index a461c50..92e5370 100644 --- a/src/index.css +++ b/src/index.css @@ -1 +1,8 @@ -@import "tailwindcss"; \ No newline at end of file + +@import "tailwindcss"; + +html, body { + height: 100%; + overflow: hidden; +} + diff --git a/vite.config.ts b/vite.config.ts index 7ef027f..7275d54 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,8 +7,8 @@ export default defineConfig({ plugins: [react(), tailwindcss()], test: { - // globals: true, + globals: true, environment: 'jsdom', - setupFiles: './src/tests/setup.ts', + setupFiles: './src/test/setup.ts', }, }) From 54cf1ccd08d55b3317af53abe1935925973a0a3e Mon Sep 17 00:00:00 2001 From: saumya-cw Date: Mon, 2 Feb 2026 14:22:02 +0530 Subject: [PATCH 2/3] fix: add error handling and request cancellation to useUsersGrid hook - Add try-catch block to handle fetch errors and log them to console - Implement AbortController for request cancellation on component unmount - Prevent silent failures by making errors observable - Ensure loading state is properly reset in finally block --- .../users-grid/components/UsersGrid.test.tsx | 13 ++++++------- src/features/users-grid/hooks/useUsersGrid.ts | 12 ++++++++++-- vite.config.ts | 1 - 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/features/users-grid/components/UsersGrid.test.tsx b/src/features/users-grid/components/UsersGrid.test.tsx index a1624f9..d81ebea 100644 --- a/src/features/users-grid/components/UsersGrid.test.tsx +++ b/src/features/users-grid/components/UsersGrid.test.tsx @@ -4,13 +4,6 @@ import { render, screen } from '@testing-library/react'; import { UsersGrid } from './UsersGrid'; import type { UserProps } from '../types/users.types'; -vi.mock('../hooks/useUsersGrid', () => ({ - useUsersApi: () => ({ - users: mockUsers, - isLoading: false, - }), -})); - const mockUsers: UserProps[] = [ { id: 1, @@ -31,6 +24,12 @@ const mockUsers: UserProps[] = [ }, ]; +vi.mock('../hooks/useUsersGrid', () => ({ + useUsersApi: () => ({ + users: mockUsers, + isLoading: false, + }), +})); vi.mock('ag-grid-react', () => ({ AgGridReact: (props: any) => ( diff --git a/src/features/users-grid/hooks/useUsersGrid.ts b/src/features/users-grid/hooks/useUsersGrid.ts index 2cbfd64..1468f03 100644 --- a/src/features/users-grid/hooks/useUsersGrid.ts +++ b/src/features/users-grid/hooks/useUsersGrid.ts @@ -9,12 +9,15 @@ export function useUsersApi() { useEffect(function () { let isMounted = true; + const abortController = new AbortController(); async function fetchUsersOnMount() { setIsLoading(true); try { - const response = await axios.get<{ users: UserProps[] }>(`${USERS_API_BASE_URL}/users`); + const response = await axios.get<{ users: UserProps[] }>(`${USERS_API_BASE_URL}/users`, { + signal: abortController.signal, + }); if (isMounted) { const mappedUsers = response.data.users.map(function (user: UserProps) { @@ -26,6 +29,10 @@ export function useUsersApi() { setUsers(mappedUsers); } + } catch (error) { + if (isMounted && !axios.isCancel(error)) { + console.error('Failed to load users', error); + } } finally { if (isMounted) { setIsLoading(false); @@ -37,6 +44,7 @@ export function useUsersApi() { return function () { isMounted = false; + abortController.abort(); }; }, []); @@ -44,4 +52,4 @@ export function useUsersApi() { users, isLoading, }; -} +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 7275d54..7421d35 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,6 @@ import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' -// https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], From b71427ed5c3756f3e1fe8ca4c59305443f574c0f Mon Sep 17 00:00:00 2001 From: saumya-cw Date: Wed, 4 Feb 2026 15:16:36 +0530 Subject: [PATCH 3/3] refactor: enhance UsersGrid component with reusable Button, utils refactoring, layout optimization, and improved tests --- package-lock.json | 10 + package.json | 1 + src/App.tsx | 8 +- src/features/users-grid/components/Button.tsx | 25 ++ .../users-grid/components/UsersGrid.test.tsx | 87 +++---- .../users-grid/components/UsersGrid.tsx | 219 ++++-------------- .../users-grid/constants/users.constants.ts | 3 - .../users-grid/utils/userColumnDefs.ts | 118 ++++++++++ 8 files changed, 252 insertions(+), 219 deletions(-) create mode 100644 src/features/users-grid/components/Button.tsx create mode 100644 src/features/users-grid/utils/userColumnDefs.ts diff --git a/package-lock.json b/package-lock.json index c4ccfae..4821fa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "ag-grid-enterprise": "^35.0.1", "ag-grid-react": "^35.0.1", "axios": "^1.13.4", + "clsx": "^2.1.1", "react": "^19.2.0", "react-dom": "^19.2.0", "tailwindcss": "^4.1.18" @@ -2750,6 +2751,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", diff --git a/package.json b/package.json index d27e8e3..947d9a2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "ag-grid-enterprise": "^35.0.1", "ag-grid-react": "^35.0.1", "axios": "^1.13.4", + "clsx": "^2.1.1", "react": "^19.2.0", "react-dom": "^19.2.0", "tailwindcss": "^4.1.18" diff --git a/src/App.tsx b/src/App.tsx index 7f1eac4..aebbd61 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,11 +3,9 @@ import { UsersGrid } from './features/users-grid/components/UsersGrid'; function App() { return ( - <> -
- -
- +
+ +
); } diff --git a/src/features/users-grid/components/Button.tsx b/src/features/users-grid/components/Button.tsx new file mode 100644 index 0000000..66a8df4 --- /dev/null +++ b/src/features/users-grid/components/Button.tsx @@ -0,0 +1,25 @@ +import clsx from "clsx"; + +type ButtonProps = { + label: string; + onClick: () => void; + isActive?: boolean; +}; + +export function Button({ label, onClick, isActive }: ButtonProps) { + const base = "px-4 py-2 text-sm rounded transition font-medium"; + + return ( + + ); +} diff --git a/src/features/users-grid/components/UsersGrid.test.tsx b/src/features/users-grid/components/UsersGrid.test.tsx index d81ebea..e155704 100644 --- a/src/features/users-grid/components/UsersGrid.test.tsx +++ b/src/features/users-grid/components/UsersGrid.test.tsx @@ -1,70 +1,79 @@ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; -import { UsersGrid } from './UsersGrid'; -import type { UserProps } from '../types/users.types'; +import { UsersGrid } from "./UsersGrid"; +import type { UserProps } from "../types/users.types"; const mockUsers: UserProps[] = [ { id: 1, - firstName: 'John', - lastName: 'Doe', - maidenName: 'Smith', + firstName: "John", + lastName: "Doe", + maidenName: "Smith", age: 30, - gender: 'male', - email: 'john@test.com', - phone: '1234567890', - username: 'johndoe', - birthDate: '1994-01-01', - bloodGroup: 'O+', + gender: "male", + email: "john@test.com", + phone: "1234567890", + username: "john doe", + birthDate: "1994-01-01", + bloodGroup: "O+", height: 180, weight: 75, - eyeColor: 'Blue', + eyeColor: "Blue", isActive: true, }, ]; -vi.mock('../hooks/useUsersGrid', () => ({ +vi.mock("../hooks/useUsersGrid", () => ({ useUsersApi: () => ({ users: mockUsers, isLoading: false, }), })); -vi.mock('ag-grid-react', () => ({ - AgGridReact: (props: any) => ( -
-
- {JSON.stringify(props.rowData)} -
-
- {JSON.stringify(props.columnDefs)} -
+vi.mock("ag-grid-react", () => ({ + AgGridReact: () => ( +
+
First Name
+
John
), })); -describe('UsersGrid', () => { - it('renders AG Grid component', () => { - render(); +const renderComponent = () => render(); - const grid = screen.getByTestId('ag-grid'); - expect(grid).toBeInTheDocument(); +describe("UsersGrid", () => { + it("renders the grid heading", () => { + renderComponent(); + + expect( + screen.getByRole("heading", { + name: /user management grid/i, + }), + ).toBeInTheDocument(); }); - it('passes row data to AG Grid', () => { - render(); + it("renders column headers", () => { + renderComponent(); - const rowData = screen.getByTestId('row-data'); - expect(rowData.textContent).toContain('John'); + expect(screen.getByText("First Name")).toBeInTheDocument(); }); - it('passes column definitions to AG Grid', () => { - render(); + it("renders user data in the grid", () => { + renderComponent(); + + expect(screen.getByText("John")).toBeInTheDocument(); + }); + + it("renders filter toggle buttons", () => { + renderComponent(); + + expect( + screen.getByRole("button", { name: /show filters/i }), + ).toBeInTheDocument(); - const columnDefs = screen.getByTestId('column-defs'); - expect(columnDefs.textContent).toContain('firstName'); - expect(columnDefs.textContent).toContain('email'); - expect(columnDefs.textContent).toContain('eyeColor'); + expect( + screen.getByRole("button", { name: /hide filters/i }), + ).toBeInTheDocument(); }); }); diff --git a/src/features/users-grid/components/UsersGrid.tsx b/src/features/users-grid/components/UsersGrid.tsx index 3ce9122..ab7f381 100644 --- a/src/features/users-grid/components/UsersGrid.tsx +++ b/src/features/users-grid/components/UsersGrid.tsx @@ -1,149 +1,40 @@ -import { useMemo, useState } from 'react'; -import { AgGridReact } from 'ag-grid-react'; -import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'; -import { SetFilterModule } from 'ag-grid-enterprise'; -import type { ColDef } from 'ag-grid-community'; +import { useState } from "react"; +import { AgGridReact } from "ag-grid-react"; +import { ModuleRegistry, AllCommunityModule } from "ag-grid-community"; +import { SetFilterModule } from "ag-grid-enterprise"; +import { useUsersApi } from "../hooks/useUsersGrid"; +import type { UserProps } from "../types/users.types"; -import { useUsersApi } from '../hooks/useUsersGrid'; -import type { UserProps } from '../types/users.types'; -import { DEFAULT_PAGINATION_PAGE_SIZE, PAGINATION_PAGE_SIZE_OPTIONS } from '../constants/users.constants'; +import { + DEFAULT_PAGINATION_PAGE_SIZE, + PAGINATION_PAGE_SIZE_OPTIONS, +} from "../constants/users.constants"; -import 'ag-grid-community/styles/ag-theme-alpine.css'; +import "ag-grid-community/styles/ag-theme-alpine.css"; +import { getUserColumnDefs, getDefaultColDef } from "../utils/userColumnDefs"; +import { Button } from "./Button"; ModuleRegistry.registerModules([AllCommunityModule, SetFilterModule]); export function UsersGrid() { - const { users, isLoading } = useUsersApi(); + const { users: userData, isLoading } = useUsersApi(); const [showFilters, setShowFilters] = useState(false); - const columnDefs = useMemo[]>( - () => [ - { - field: 'id', - headerName: 'ID', - width: 90, - filter: showFilters ? 'agNumberColumnFilter' : false, - }, - { - field: 'firstName', - headerName: 'First Name', - minWidth: 160, - filter: showFilters ? 'agTextColumnFilter' : false, - }, - { - field: 'lastName', - headerName: 'Last Name', - minWidth: 160, - filter: showFilters ? 'agTextColumnFilter' : false, - }, - { - field: 'maidenName', - headerName: 'Maiden Name', - minWidth: 160, - filter: showFilters ? 'agTextColumnFilter' : false, - }, - { - field: 'age', - headerName: 'Age', - width: 100, - filter: showFilters ? 'agNumberColumnFilter' : false, - }, - { - field: 'gender', - headerName: 'Gender', - minWidth: 140, - filter: showFilters ? 'agSetColumnFilter' : false, - }, - { - field: 'email', - headerName: 'Email', - minWidth: 240, - filter: showFilters ? 'agTextColumnFilter' : false, - }, - { - field: 'phone', - headerName: 'Phone', - minWidth: 160, - filter: showFilters ? 'agTextColumnFilter' : false, - }, - { - field: 'username', - headerName: 'Username', - minWidth: 140, - filter: showFilters ? 'agTextColumnFilter' : false, - }, - { - field: 'birthDate', - headerName: 'Birth Date', - minWidth: 140, - filter: showFilters ? 'agTextColumnFilter' : false, - }, - { - field: 'bloodGroup', - headerName: 'Blood Group', - minWidth: 120, - filter: showFilters ? 'agTextColumnFilter' : false, - }, - { - field: 'height', - headerName: 'Height', - width: 100, - filter: showFilters ? 'agNumberColumnFilter' : false, - }, - { - field: 'weight', - headerName: 'Weight', - width: 100, - filter: showFilters ? 'agNumberColumnFilter' : false, - }, - { - field: 'eyeColor', - headerName: 'Eye Color', - minWidth: 120, - filter: showFilters ? 'agTextColumnFilter' : false, - cellStyle: (params) => { - const color = String(params.value).toLowerCase(); - const backgroundMap: Record = { - blue: '#dbeafe', - brown: '#fef3c7', - green: '#dcfce7', - hazel: '#fef9c3', - gray: '#e5e7eb', - black: '#e5e7eb', - }; - return { - backgroundColor: backgroundMap[color] ?? '#f3f4f6', - color: '#111827', - fontWeight: 500, - textTransform: 'capitalize', - }; - }, - }, - { - field: 'isActive', - headerName: 'Active', - width: 120, - filter: showFilters ? 'agTextColumnFilter' : false, - cellRenderer: (params: any) => - params.value ? ( - Active - ) : ( - Inactive - ), - }, - ], - [showFilters] - ); + const columnDefs = getUserColumnDefs(showFilters); + const defaultColDef = getDefaultColDef(showFilters); - const defaultColDef = useMemo( - () => ({ - sortable: true, - resizable: true, - filter: showFilters, - floatingFilter: showFilters, - }), - [showFilters] - ); + const filterButtons = [ + { + label: "Show Filters", + isActive: showFilters, + onClick: () => setShowFilters(true), + }, + { + label: "Hide Filters", + isActive: !showFilters, + onClick: () => setShowFilters(false), + }, + ]; return (
@@ -154,46 +45,30 @@ export function UsersGrid() {
- - - + {filterButtons.map(({ label, onClick, isActive }) => ( +
{/* Grid Wrapper */}
-
- - rowData={users} - columnDefs={columnDefs} - defaultColDef={defaultColDef} - loading={isLoading} - pagination - paginationPageSize={DEFAULT_PAGINATION_PAGE_SIZE} - paginationPageSizeSelector={PAGINATION_PAGE_SIZE_OPTIONS} - /> -
+ + className="ag-theme-alpine min-w-300 rounded-lg border shadow-sm bg-violet-500" + containerStyle={{ height: 600 }} + rowData={userData} + columnDefs={columnDefs} + defaultColDef={defaultColDef} + loading={isLoading} + pagination + paginationPageSize={DEFAULT_PAGINATION_PAGE_SIZE} + paginationPageSizeSelector={PAGINATION_PAGE_SIZE_OPTIONS} + />
); diff --git a/src/features/users-grid/constants/users.constants.ts b/src/features/users-grid/constants/users.constants.ts index bca3d4f..d08918b 100644 --- a/src/features/users-grid/constants/users.constants.ts +++ b/src/features/users-grid/constants/users.constants.ts @@ -1,11 +1,8 @@ -// User-related constants for the users grid feature - export const USERS_API_BASE_URL = 'https://dummyjson.com'; export const DEFAULT_PAGINATION_PAGE_SIZE = 10; export const PAGINATION_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; - export const Gender = { Male: 'male', Female: 'female', diff --git a/src/features/users-grid/utils/userColumnDefs.ts b/src/features/users-grid/utils/userColumnDefs.ts new file mode 100644 index 0000000..21a6abe --- /dev/null +++ b/src/features/users-grid/utils/userColumnDefs.ts @@ -0,0 +1,118 @@ +import type { ColDef } from 'ag-grid-community' +import type { UserProps } from '../types/users.types' + +export function getUserColumnDefs( + showFilters: boolean +): ColDef[] { + return [ + { + field: 'id', + headerName: 'ID', + width: 90, + filter: showFilters ? 'agNumberColumnFilter' : false, + }, + { + field: 'firstName', + headerName: 'First Name', + minWidth: 160, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'lastName', + headerName: 'Last Name', + minWidth: 160, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'maidenName', + headerName: 'Maiden Name', + minWidth: 160, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'age', + headerName: 'Age', + width: 100, + filter: showFilters ? 'agNumberColumnFilter' : false, + }, + { + field: 'gender', + headerName: 'Gender', + minWidth: 140, + filter: showFilters ? 'agSetColumnFilter' : false, + }, + { + field: 'email', + headerName: 'Email', + minWidth: 240, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'phone', + headerName: 'Phone', + minWidth: 160, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'username', + headerName: 'Username', + minWidth: 140, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'birthDate', + headerName: 'Birth Date', + minWidth: 140, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'bloodGroup', + headerName: 'Blood Group', + minWidth: 120, + filter: showFilters ? 'agTextColumnFilter' : false, + }, + { + field: 'height', + headerName: 'Height', + width: 100, + filter: showFilters ? 'agNumberColumnFilter' : false, + }, + { + field: 'weight', + headerName: 'Weight', + width: 100, + filter: showFilters ? 'agNumberColumnFilter' : false, + }, + { + field: 'eyeColor', + headerName: 'Eye Color', + minWidth: 120, + filter: showFilters ? 'agTextColumnFilter' : false, + cellStyle: (params) => { + const color = String(params.value).toLowerCase(); + const backgroundMap: Record = { + blue: '#dbeafe', + brown: '#fef3c7', + green: '#dcfce7', + hazel: '#fef9c3', + gray: '#e5e7eb', + black: '#e5e7eb', + }; + return { + backgroundColor: backgroundMap[color] ?? '#f3f4f6', + color: '#111827', + fontWeight: 500, + textTransform: 'capitalize', + }; + }, + }, + ] +} +export function getDefaultColDef(showFilters: boolean) { + return { + sortable: true, + resizable: true, + filter: showFilters, + floatingFilter: showFilters, + }; +}