Skip to content
Open
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
1 change: 1 addition & 0 deletions src/components/Icon.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export const Icon = (props: IconProps) => {
"menu",
"tile",
"list",
"cards",
];
const weatherIcons: IconProps["icon"][] = [
"fog",
Expand Down
5 changes: 5 additions & 0 deletions src/components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,11 @@ export const Icons = {
<path d="M4 6H20V8H4V6ZM4 11H20V13H4V11ZM4 16H20V18H4V16Z" />
</>
),
cards: (
<>
<path d="M10 13C10.5523 13 11 13.4477 11 14V20C11 20.5523 10.5523 21 10 21H4C3.44772 21 3 20.5523 3 20V14C3 13.4477 3.44772 13 4 13H10ZM20 13C20.5523 13 21 13.4477 21 14V20C21 20.5523 20.5523 21 20 21H14C13.4477 21 13 20.5523 13 20V14C13 13.4477 13.4477 13 14 13H20ZM15 19H19V15H15V19ZM5 19H9V15H5V19ZM10 3C10.5523 3 11 3.44772 11 4V10C11 10.5523 10.5523 11 10 11H4C3.44772 11 3 10.5523 3 10V4C3 3.44772 3.44772 3 4 3H10ZM20 3C20.5523 3 21 3.44772 21 4V10C21 10.5523 20.5523 11 20 11H14C13.4477 11 13 10.5523 13 10V4C13 3.44772 13.4477 3 14 3H20ZM15 9H19V5H15V9ZM5 9H9V5H5V9Z" />
</>
),
// Weather
cloudy: (
<>
Expand Down
115 changes: 114 additions & 1 deletion src/components/Layout/GridTableLayout/GridTableLayout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Css } from "src/Css";
import { noop } from "src/utils";
import { withBeamDecorator, withRouter, zeroTo } from "src/utils/sb";
import { TestProjectLayout } from "../Layout.stories";
import { GridTableLayout as GridTableLayoutComponent, useGridTableLayoutState } from "./GridTableLayout";
import { CardItem, GridTableLayout as GridTableLayoutComponent, useGridTableLayoutState } from "./GridTableLayout";

export default {
component: GridTableLayoutComponent,
Expand Down Expand Up @@ -561,3 +561,116 @@ function makeNestedRows(repeat: number = 1): GridDataRow<Row>[] {
];
});
}

const sampleCards: CardItem[] = [
{
id: "1",
image: "plan-exterior.png",
title: "The Cora Plan",
description: "SFH-001 - 4,000-5,000sf, 5-6bd - Luxury single family home with premium finishes",
},
{
id: "2",
image: "plan-exterior.png",
title: "The Conroy Plan",
description: "SFH-002 - 4,000-5,000sf, 4-5bd - Traditional style with modern amenities",
},
{
id: "3",
image: "plan-exterior.png",
title: "The Rayburn Plan",
description: "SFH-003 - 2,800-3,200sf, 3-4bd - Contemporary design with open floor plan",
},
{
id: "4",
image: "plan-exterior.png",
title: "The Madison Plan",
description: "SFH-004 - 3,500-4,000sf, 5-6bd - Luxury single family home with premium finishes",
},
{
id: "5",
image: "plan-exterior.png",
title: "The Emerson Plan",
description: "SFH-005 - 2,800-3,200sf, 4-5bd - Traditional style with modern amenities",
},
{
id: "6",
image: "plan-exterior.png",
title: "The Hamilton Plan",
description: "SFH-006 - 2,800-3,200sf, 3-4bd - Contemporary design with open floor plan",
},
];

export function GridTableLayoutWithCardView() {
const filterDefs = useMemo(() => getFilterDefs(), []);
const columns = useMemo(() => getColumns(), []);
const layoutState = useGridTableLayoutState({
persistedFilter: { filterDefs, storageKey: "grid-table-layout-card" },
search: "client",
});

return (
<TestProjectLayout>
<GridTableLayoutComponent
pageTitle="Grid Table Layout with Cards View"
breadcrumb={[
{ href: "/", label: "Home" },
{ href: "/", label: "Products" },
]}
layoutState={layoutState}
tableProps={{
columns: [collapseColumn<Row>(), selectColumn<Row>(), ...columns],
rows: [simpleHeader, ...makeNestedRows(3)],
sorting: { on: "client", initial: [columns[1].id!, "ASC"] },
}}
cardView={{ cards: sampleCards }}
primaryAction={{ label: "Add Product", onClick: noop }}
/>
</TestProjectLayout>
);
}

export function CardsViewWithSidePanel() {
const columns = useMemo(() => getColumns(), []);
const [selectedCard, setSelectedCard] = useState<CardItem | null>(null);
const layoutState = useGridTableLayoutState({ search: "client" });
const clickableCards: CardItem[] = sampleCards.map((card) => ({
...card,
onClick: () => setSelectedCard(card),
}));

const sidePanel = selectedCard ? (
<div css={Css.bgWhite.br8.m2.p3.df.fdc.gap2.bshBasic.$}>
<h2 css={Css.lg.gray900.$}>{selectedCard?.title}</h2>
<img src={selectedCard.image} alt={selectedCard.title} css={Css.w100.br8.$} />
<p css={Css.sm.gray700.$}>{selectedCard.description}</p>
<div css={Css.ba.bcGray200.br8.p3.mt2.$}>
<div css={Css.smSb.gray900.$}>Details</div>
<div css={Css.sm.gray700.mt2.$}>
<p>ID: {selectedCard.id}</p>
<p>Status: Active</p>
<p>Last Updated: Today</p>
</div>
</div>
</div>
) : (
<div css={Css.h100.bgGray100.br8.p3.df.aic.jcc.$}>
<p css={Css.sm.gray500.$}>Click a card to see details</p>
</div>
);

return (
<TestProjectLayout>
<GridTableLayoutComponent
pageTitle="Cards View with Side Panel Example"
breadcrumb={[{ href: "/", label: "Home" }]}
layoutState={layoutState}
tableProps={{
columns,
rows: [simpleHeader, ...makeNestedRows(1)],
}}
cardView={{ cards: clickableCards, sidePanel }}
/>
</TestProjectLayout>
);
}
92 changes: 92 additions & 0 deletions src/components/Layout/GridTableLayout/GridTableLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { noop } from "src/utils";
import { click, render, tableSnapshot, withRouter } from "src/utils/rtl";
import { QueryParamProvider } from "use-query-params";
import {
CardItem,
GridTableLayout as GridTableLayoutComponent,
GridTableLayoutProps,
useGridTableLayoutState,
Expand Down Expand Up @@ -247,6 +248,97 @@ describe("GridTableLayout", () => {
});
});

describe("Card View", () => {
const sampleCards: CardItem[] = [
{ id: "card1", image: "image1.png", title: "Card 1", description: "Description 1" },
{ id: "card2", image: "image2.png", title: "Card 2", description: "Description 2" },
];

it("does not show view toggle when cardView is not provided", async () => {
// Given a GridTableLayout without cardView set
// When the component is rendered
const r = await render(
<QueryParamProvider>
<TestWrapper
layoutStateProps={{ search: "client" }}
pageTitle="Test"
tableProps={{ columns, rows: [simpleHeader, ...rows] }}
/>
</QueryParamProvider>,
withRouter(),
);
// Then the view toggle is not rendered
expect(r.query.viewToggle).not.toBeInTheDocument();
});

it("switches to card view when toggle is clicked", async () => {
// Given a GridTableLayout with cardView set
const r = await render(
<QueryParamProvider>
<TestWrapper
layoutStateProps={{}}
pageTitle="Test"
tableProps={{ columns, rows: [simpleHeader, ...rows] }}
cardView={{ cards: sampleCards }}
/>
</QueryParamProvider>,
withRouter(),
);
// When clicking the card view button
click(r.viewToggle_cards);
// Then cards are rendered
expect(r.cardGridView_card_0).toBeInTheDocument();
expect(r.cardGridView_card_1).toBeInTheDocument();
// And the card content is visible
expect(r.cardGridView_cardTitle_0).toHaveTextContent("Card 1");
expect(r.cardGridView_cardDescription_0).toHaveTextContent("Description 1");
});

it("hides EditColumnsButton in card view", async () => {
// Given a GridTableLayout with cardView and hideable columns
const hideableColumns = columns.map((c) => ({ ...c, canHide: true }));
const r = await render(
<QueryParamProvider>
<TestWrapper
layoutStateProps={{}}
pageTitle="Test"
tableProps={{ columns: hideableColumns, rows: [simpleHeader, ...rows] }}
cardView={{ cards: sampleCards }}
/>
</QueryParamProvider>,
withRouter(),
);
// Then EditColumnsButton is visible in table view
expect(r.editColumnsButton).toBeInTheDocument();
// When switching to card view
click(r.viewToggle_cards);
// Then EditColumnsButton is hidden
expect(r.query.editColumnsButton).not.toBeInTheDocument();
});

it("renders side panel only in card view", async () => {
// Given a GridTableLayout with cardView and a side panel
const r = await render(
<QueryParamProvider>
<TestWrapper
layoutStateProps={{}}
pageTitle="Test"
tableProps={{ columns, rows: [simpleHeader, ...rows] }}
cardView={{ cards: sampleCards, sidePanel: <div>Side Panel Content</div> }}
/>
</QueryParamProvider>,
withRouter(),
);
// Then the side panel is not visible in table view
expect(r.query.cardGridView_sidePanel).not.toBeInTheDocument();
// And when switching to card view
click(r.viewToggle_cards);
// Then the side panel is visible
expect(r.cardGridView_sidePanel).toBeInTheDocument();
expect(r.cardGridView_sidePanel).toHaveTextContent("Side Panel Content");
});
});

describe("pagination", () => {
it("renders pagination when totalCount is provided", async () => {
// Given a GridTableLayout with totalCount
Expand Down
Loading