Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
116 changes: 115 additions & 1 deletion src/components/Layout/GridTableLayout/GridTableLayout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { checkboxFilter, multiFilter } from "src/components/Filters";
import { GridDataRow } from "src/components/Table";
import { collapseColumn, column, numericColumn, selectColumn } from "src/components/Table/utils/columns";
import { simpleHeader } from "src/components/Table/utils/simpleHelpers";
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 @@ -240,3 +241,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>
);
}
94 changes: 93 additions & 1 deletion src/components/Layout/GridTableLayout/GridTableLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { checkboxFilter } from "src/components/Filters";
import { actionColumn, column, numericColumn } from "src/components/Table/utils/columns";
import { simpleHeader } from "src/components/Table/utils/simpleHelpers";
import { noop } from "src/utils";
import { render, tableSnapshot, withRouter } from "src/utils/rtl";
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 @@ -240,4 +241,95 @@ describe("GridTableLayout", () => {
expect(r.pageTitle).toBeInTheDocument();
});
});

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");
});
});
});
Loading