From 95fa7a056873dff9465c1c59ee76cbe717fbb8c8 Mon Sep 17 00:00:00 2001 From: Anuj Rajak Date: Mon, 13 Oct 2025 14:36:16 +0530 Subject: [PATCH] Set of Cards Component --- README.md | 50 +++++- src/components/SetOfCardsWrapper.tsx | 69 ++++++++ src/constants/componentsMap.ts | 6 +- src/global.css | 46 ++++++ .../components/SetOfCardsWrapper.test.tsx | 154 ++++++++++++++++++ 5 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 src/components/SetOfCardsWrapper.tsx create mode 100644 src/test/components/SetOfCardsWrapper.test.tsx diff --git a/README.md b/README.md index 6598569..cb6a40d 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,14 @@ This npm package provides a collection of reusable Patternfly React components t - ImageComponent - TableWrapper - VideoPlayerWrapper + - SetOfCardsWrapper + * Dynamic Component Renderer - DynamicComponents * Supported Components - - `one-card`, `image`, `table`, `video-player` + - `one-card`, `image`, `table`, `video-player`, `set-of-cards` - `video-player` supports YouTube video URLs and direct video file URLs + - `set-of-cards` displays multiple OneCard components in an auto-aligned grid layout ## Installation @@ -131,13 +134,13 @@ function App() { ### VideoPlayer Component ```jsx -import { VideoPlayerWrapper } from '@rhngui/patternfly-react-renderer'; +import { VideoPlayerWrapper } from "@rhngui/patternfly-react-renderer"; const videoData = { component: "video-player", video: "https://www.youtube.com/embed/v-PjgYDrg70", video_img: "https://img.youtube.com/vi/v-PjgYDrg70/maxresdefault.jpg", - title: "Toy Story Trailer" + title: "Toy Story Trailer", }; function App() { @@ -145,6 +148,47 @@ function App() { } ``` +### SetOfCards Component + +```jsx +import { DynamicComponent } from "@rhngui/patternfly-react-renderer"; + +const setOfCardsConfig = { + component: "set-of-cards", + id: "test-id", + title: "My Favorite Movies", + fields: [ + { + data: ["Toy Story", "My Name is Khan"], + data_path: "movie.title", + name: "Title", + }, + { + data: [1995, 2003], + data_path: "movie.year", + name: "Year", + }, + { + data: [8.3, 8.5], + data_path: "movie.imdbRating", + name: "IMDB Rating", + }, + { + data: [ + ["Jim Varney", "Tim Allen", "Tom Hanks", "Don Rickles"], + ["Shah Rukh Khan", "Kajol Devgan"], + ], + data_path: "actors[*]", + name: "Actors", + }, + ], +}; + +function App() { + return ; +} +``` + ## Links - [Documentation](https://redhat-ux.github.io/next-gen-ui-agent/guide/renderer/patternfly_npm/) diff --git a/src/components/SetOfCardsWrapper.tsx b/src/components/SetOfCardsWrapper.tsx new file mode 100644 index 0000000..c9b6605 --- /dev/null +++ b/src/components/SetOfCardsWrapper.tsx @@ -0,0 +1,69 @@ +import OneCardWrapper from "./OneCardWrapper"; + +interface FieldData { + name: string; + data_path: string; + data: (string | number | boolean | null | (string | number)[])[]; +} + +interface SetOfCardsWrapperProps { + component: "set-of-cards"; + id: string; + title: string; + fields: FieldData[]; + className?: string; +} + +const SetOfCardsWrapper = (props: SetOfCardsWrapperProps) => { + const { title, id, fields, className } = props; + + // Transform fields data into individual card data + const transformFieldsToCardsData = () => { + if (!fields || fields.length === 0) return []; + + // Find the maximum number of data items across all fields + const maxDataLength = Math.max(...fields.map((field) => field.data.length)); + + // Create individual card data for each row + const cardsData = []; + for (let i = 0; i < maxDataLength; i++) { + const cardFields = fields.map((field) => { + const item = field.data[i]; + // If the item is an array, use it directly; otherwise wrap it in an array + const data = Array.isArray(item) ? item : [item]; + return { + name: field.name, + data_path: field.data_path, + data: data, + }; + }); + + cardsData.push({ + title: `${title} ${i + 1}`, + fields: cardFields, + id: `${id}-card-${i}`, + }); + } + + return cardsData; + }; + + const cardsData = transformFieldsToCardsData(); + + return ( +
+

{title}

+
+ {cardsData.map((cardData, index) => ( + + ))} +
+
+ ); +}; + +export default SetOfCardsWrapper; diff --git a/src/constants/componentsMap.ts b/src/constants/componentsMap.ts index 2321da3..1f2e519 100644 --- a/src/constants/componentsMap.ts +++ b/src/constants/componentsMap.ts @@ -1,11 +1,13 @@ import ImageComponent from "../components/ImageComponent"; import OneCardWrapper from "../components/OneCardWrapper"; +import SetOfCardsWrapper from "../components/SetOfCardsWrapper"; import TableWrapper from "../components/TableWrapper"; import VideoPlayerWrapper from "../components/VideoPlayerWrapper"; export const componentsMap = { "one-card": OneCardWrapper, - "table": TableWrapper, - "image": ImageComponent, + table: TableWrapper, + image: ImageComponent, "video-player": VideoPlayerWrapper, + "set-of-cards": SetOfCardsWrapper, }; diff --git a/src/global.css b/src/global.css index 1c27df9..2ca3eff 100644 --- a/src/global.css +++ b/src/global.css @@ -97,6 +97,52 @@ object-fit: cover; } +/* Set of Cards Component Styles */ +.set-of-cards-container { + max-width: var(--ngui-container-max-width); + margin: 0 auto; + width: 100%; + padding: var(--ngui-spacing-medium); +} + +.set-of-cards-title { + margin-bottom: var(--ngui-spacing-medium); + font-size: var(--pf-global--FontSize--xl); + font-weight: var(--pf-global--FontWeight--bold); + color: var(--pf-global--Color--100); +} + +.set-of-cards-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--ngui-spacing-medium); + align-items: start; +} + +.set-of-cards-item { + width: 100%; + height: fit-content; +} + +/* Responsive adjustments for set-of-cards */ +@media (max-width: 768px) { + .set-of-cards-grid { + grid-template-columns: 1fr; + gap: calc(var(--ngui-spacing-medium) * 0.75); + } + + .set-of-cards-container { + padding: calc(var(--ngui-spacing-medium) * 0.75); + } +} + +@media (min-width: 1200px) { + .set-of-cards-grid { + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: calc(var(--ngui-spacing-medium) * 1.25); + } +} + /* Common Error Handling Styles */ .error-placeholder { width: 100%; diff --git a/src/test/components/SetOfCardsWrapper.test.tsx b/src/test/components/SetOfCardsWrapper.test.tsx new file mode 100644 index 0000000..7b2f01b --- /dev/null +++ b/src/test/components/SetOfCardsWrapper.test.tsx @@ -0,0 +1,154 @@ +import { render, screen } from "@testing-library/react"; + +import SetOfCardsWrapper from "../../components/SetOfCardsWrapper"; + +describe("SetOfCardsWrapper", () => { + const mockProps = { + component: "set-of-cards" as const, + id: "test-set-of-cards", + title: "Test Movies", + fields: [ + { + name: "Title", + data_path: "movie.title", + data: ["Toy Story", "Finding Nemo"], + }, + { + name: "Year", + data_path: "movie.year", + data: [1995, 2003], + }, + { + name: "Rating", + data_path: "movie.rating", + data: [8.3, 8.1], + }, + ], + }; + + it("renders the component with correct title", () => { + render(); + expect(screen.getByText("Test Movies")).toBeInTheDocument(); + }); + + it("renders the correct number of cards", () => { + render(); + // Should render 2 cards based on the data length + expect(screen.getByText("Test Movies 1")).toBeInTheDocument(); + expect(screen.getByText("Test Movies 2")).toBeInTheDocument(); + }); + + it("renders cards with correct field data", () => { + render(); + + // Check first card data + expect(screen.getByText("Toy Story")).toBeInTheDocument(); + expect(screen.getByText("1995")).toBeInTheDocument(); + expect(screen.getByText("8.3")).toBeInTheDocument(); + + // Check second card data + expect(screen.getByText("Finding Nemo")).toBeInTheDocument(); + expect(screen.getByText("2003")).toBeInTheDocument(); + expect(screen.getByText("8.1")).toBeInTheDocument(); + }); + + it("handles empty fields array", () => { + const emptyProps = { + ...mockProps, + fields: [], + }; + render(); + expect(screen.getByText("Test Movies")).toBeInTheDocument(); + // Should not render any cards + expect(screen.queryByText("Test Movies 1")).not.toBeInTheDocument(); + }); + + it("handles fields with different data lengths", () => { + const unevenProps = { + ...mockProps, + fields: [ + { + name: "Title", + data_path: "movie.title", + data: ["Movie 1", "Movie 2", "Movie 3"], + }, + { + name: "Year", + data_path: "movie.year", + data: [1995, 2003], // Shorter array + }, + ], + }; + render(); + + // Should render 3 cards based on the longest data array + expect(screen.getByText("Test Movies 1")).toBeInTheDocument(); + expect(screen.getByText("Test Movies 2")).toBeInTheDocument(); + expect(screen.getByText("Test Movies 3")).toBeInTheDocument(); + }); + + it("handles array data correctly", () => { + const arrayProps = { + ...mockProps, + fields: [ + { + name: "Actors", + data_path: "movie.actors", + data: [ + ["Tom Hanks", "Tim Allen"], + ["Albert Brooks", "Ellen DeGeneres"], + ], + }, + ], + }; + render(); + + // Arrays should be joined with commas + expect(screen.getByText("Tom Hanks, Tim Allen")).toBeInTheDocument(); + expect( + screen.getByText("Albert Brooks, Ellen DeGeneres") + ).toBeInTheDocument(); + }); + + it("handles null/undefined values", () => { + const nullProps = { + ...mockProps, + fields: [ + { + name: "Title", + data_path: "movie.title", + data: ["Movie 1", null, "Movie 3"], + }, + ], + }; + render(); + + // Should render 3 cards, with empty content for null value + expect(screen.getByText("Test Movies 1")).toBeInTheDocument(); + expect(screen.getByText("Test Movies 2")).toBeInTheDocument(); + expect(screen.getByText("Test Movies 3")).toBeInTheDocument(); + }); + + it("applies custom className", () => { + const customProps = { + ...mockProps, + className: "custom-class", + }; + const { container } = render(); + expect(container.firstChild).toHaveClass("custom-class"); + }); + + it("renders with correct container structure", () => { + const { container } = render(); + + // Check container structure + const containerElement = container.querySelector("#test-set-of-cards"); + expect(containerElement).toBeInTheDocument(); + expect(containerElement).toHaveClass("set-of-cards-container"); + + // Check grid structure + const gridElement = container.querySelector(".set-of-cards-grid"); + expect(gridElement).toBeInTheDocument(); + }); +}); +