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();
+ });
+});
+