mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Introduce helper component to render list of React elements
Features: - Placeholder for empty list - Separators between items - No boilerplate for "key" prop in React Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
43e0c49f06
commit
76991b59e6
58
src/renderer/components/map/__snapshots__/map.test.tsx.snap
Normal file
58
src/renderer/components/map/__snapshots__/map.test.tsx.snap
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Map given items and placeholder but no separator renders 1`] = `
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-testid="some-item-id"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
data-testid="some-other-item-id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Map given more than one item and separator renders 1`] = `
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-testid="some-item-id"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
data-testid="separator"
|
||||||
|
>
|
||||||
|
Some separator
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-testid="some-other-item-id"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
data-testid="separator"
|
||||||
|
>
|
||||||
|
Some separator
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-testid="some-another-item-id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Map given no items and placeholder renders 1`] = `
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-testid="some-placeholder"
|
||||||
|
>
|
||||||
|
Some placeholder
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Map given no items but no placeholder renders 1`] = `
|
||||||
|
<body>
|
||||||
|
<div />
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { addSeparator } from "./add-separator";
|
||||||
|
|
||||||
|
describe("add-separator", () => {
|
||||||
|
it("given multiple items, adds separators", () => {
|
||||||
|
const items = ["first", "second", "third"];
|
||||||
|
|
||||||
|
const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
|
||||||
|
|
||||||
|
expect(actual).toEqual([
|
||||||
|
"first",
|
||||||
|
"separator-between-first-and-second",
|
||||||
|
"second",
|
||||||
|
"separator-between-second-and-third",
|
||||||
|
"third",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given multiple items including falsy ones, adds separators", () => {
|
||||||
|
const items = [false, undefined, null, NaN];
|
||||||
|
|
||||||
|
const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
|
||||||
|
|
||||||
|
expect(actual).toEqual([
|
||||||
|
false,
|
||||||
|
"separator-between-false-and-undefined",
|
||||||
|
undefined,
|
||||||
|
"separator-between-undefined-and-null",
|
||||||
|
null,
|
||||||
|
"separator-between-null-and-NaN",
|
||||||
|
NaN,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given no items, does not add separator", () => {
|
||||||
|
const items: any[] = [];
|
||||||
|
|
||||||
|
const actual = addSeparator(() => "separator", items);
|
||||||
|
|
||||||
|
expect(actual).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given one item, does not add separator", () => {
|
||||||
|
const items = ["first"];
|
||||||
|
|
||||||
|
const actual = addSeparator(() => "separator", items);
|
||||||
|
|
||||||
|
expect(actual).toEqual(["first"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
26
src/renderer/components/map/add-separator/add-separator.ts
Normal file
26
src/renderer/components/map/add-separator/add-separator.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type GetSeparator<Item, Separator> = (left: Item, right: Item) => Separator;
|
||||||
|
|
||||||
|
export const addSeparator = <Item, Separator>(
|
||||||
|
getSeparator: GetSeparator<Item, Separator>,
|
||||||
|
items: Item[],
|
||||||
|
) => items.flatMap(toSeparatedTupleUsing(getSeparator));
|
||||||
|
|
||||||
|
const toSeparatedTupleUsing =
|
||||||
|
<Item, Separator>(getSeparator: GetSeparator<Item, Separator>) =>
|
||||||
|
(leftItem: Item, index: number, arr: Item[]) => {
|
||||||
|
const itemIsLast = arr.length === index + 1;
|
||||||
|
|
||||||
|
if (itemIsLast) {
|
||||||
|
return [leftItem];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightItem = arr[index + 1];
|
||||||
|
const separator = getSeparator(leftItem, rightItem);
|
||||||
|
|
||||||
|
return [leftItem, separator];
|
||||||
|
};
|
||||||
121
src/renderer/components/map/map.test.tsx
Normal file
121
src/renderer/components/map/map.test.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { RenderResult } from "@testing-library/react";
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import React from "react";
|
||||||
|
import { Map } from "./map";
|
||||||
|
|
||||||
|
describe("Map", () => {
|
||||||
|
describe("given no items and placeholder", () => {
|
||||||
|
let rendered: RenderResult;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
rendered = render(
|
||||||
|
<Map
|
||||||
|
items={[]}
|
||||||
|
getPlaceholder={() => (
|
||||||
|
<div data-testid="some-placeholder">Some placeholder</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{() => <div data-testid="some-row">Irrelevant</div>}
|
||||||
|
</Map>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders", () => {
|
||||||
|
expect(rendered.baseElement).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders placeholder", () => {
|
||||||
|
expect(rendered.getByTestId("some-placeholder")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render rows", () => {
|
||||||
|
expect(rendered.queryByTestId("some-row")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given no items but no placeholder", () => {
|
||||||
|
let rendered: RenderResult;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
rendered = render(
|
||||||
|
<Map items={[]}>
|
||||||
|
{() => <div data-testid="some-row">Irrelevant</div>}
|
||||||
|
</Map>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders", () => {
|
||||||
|
expect(rendered.baseElement).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render rows", () => {
|
||||||
|
expect(rendered.queryByTestId("some-row")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given items and placeholder but no separator", () => {
|
||||||
|
let rendered: RenderResult;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
rendered = render(
|
||||||
|
<Map
|
||||||
|
items={[{ id: "some-item-id" }, { id: "some-other-item-id" }]}
|
||||||
|
getPlaceholder={() => (
|
||||||
|
<div data-testid="some-placeholder">Some placeholder</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(item) => <div data-testid={item.id} />}
|
||||||
|
</Map>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders", () => {
|
||||||
|
expect(rendered.baseElement).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render placeholder", () => {
|
||||||
|
expect(
|
||||||
|
rendered.queryByTestId("some-placeholder"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders items", () => {
|
||||||
|
expect(rendered.getByTestId("some-item-id")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given more than one item and separator", () => {
|
||||||
|
let rendered: RenderResult;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
rendered = render(
|
||||||
|
<Map
|
||||||
|
items={[
|
||||||
|
{ id: "some-item-id" },
|
||||||
|
{ id: "some-other-item-id" },
|
||||||
|
{ id: "some-another-item-id" },
|
||||||
|
]}
|
||||||
|
getSeparator={() => <div data-testid="separator">Some separator</div>}
|
||||||
|
>
|
||||||
|
{(item) => <div data-testid={item.id} />}
|
||||||
|
</Map>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders", () => {
|
||||||
|
expect(rendered.baseElement).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders items", () => {
|
||||||
|
expect(rendered.getByTestId("some-item-id")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders separator", () => {
|
||||||
|
expect(rendered.getAllByTestId("separator")).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
58
src/renderer/components/map/map.tsx
Normal file
58
src/renderer/components/map/map.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
import { identity, map } from "lodash/fp";
|
||||||
|
import React from "react";
|
||||||
|
import { addSeparator } from "./add-separator/add-separator";
|
||||||
|
|
||||||
|
interface RequiredPropertiesForItem {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MapProps<Item extends RequiredPropertiesForItem> {
|
||||||
|
items: Item[];
|
||||||
|
children: (item: Item) => React.ReactElement;
|
||||||
|
getPlaceholder?: () => React.ReactElement;
|
||||||
|
getSeparator?: () => React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Map = <Item extends RequiredPropertiesForItem>(
|
||||||
|
props: MapProps<Item>,
|
||||||
|
) => {
|
||||||
|
const { items, getPlaceholder, getSeparator, children } = props;
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return getPlaceholder?.() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{pipeline(
|
||||||
|
items,
|
||||||
|
|
||||||
|
map((item) => ({ item, render: () => children(item) })),
|
||||||
|
|
||||||
|
getSeparator
|
||||||
|
? (items) =>
|
||||||
|
addSeparator(
|
||||||
|
(left, right) => ({
|
||||||
|
item: {
|
||||||
|
id: `separator-between-${left.item.id}-and-${right.item.id}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
render: getSeparator,
|
||||||
|
}),
|
||||||
|
|
||||||
|
items,
|
||||||
|
)
|
||||||
|
: identity,
|
||||||
|
|
||||||
|
map(({ render, item }) => (
|
||||||
|
<React.Fragment key={item.id}>{render()}</React.Fragment>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user