diff --git a/src/renderer/components/map/__snapshots__/map.test.tsx.snap b/src/renderer/components/map/__snapshots__/map.test.tsx.snap new file mode 100644 index 0000000000..e6e69f554a --- /dev/null +++ b/src/renderer/components/map/__snapshots__/map.test.tsx.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Map given items and placeholder but no separator renders 1`] = ` + +
+
+
+
+ +`; + +exports[`Map given more than one item and separator renders 1`] = ` + +
+
+
+ Some separator +
+
+
+ Some separator +
+
+
+ +`; + +exports[`Map given no items and placeholder renders 1`] = ` + +
+
+ Some placeholder +
+
+ +`; + +exports[`Map given no items but no placeholder renders 1`] = ` + +
+ +`; diff --git a/src/renderer/components/map/add-separator/add-separator.test.ts b/src/renderer/components/map/add-separator/add-separator.test.ts new file mode 100644 index 0000000000..8a45b45739 --- /dev/null +++ b/src/renderer/components/map/add-separator/add-separator.test.ts @@ -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"]); + }); +}); diff --git a/src/renderer/components/map/add-separator/add-separator.ts b/src/renderer/components/map/add-separator/add-separator.ts new file mode 100644 index 0000000000..d0d87c9a3f --- /dev/null +++ b/src/renderer/components/map/add-separator/add-separator.ts @@ -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 = (left: Item, right: Item) => Separator; + +export const addSeparator = ( + getSeparator: GetSeparator, + items: Item[], +) => items.flatMap(toSeparatedTupleUsing(getSeparator)); + +const toSeparatedTupleUsing = + (getSeparator: GetSeparator) => + (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]; + }; diff --git a/src/renderer/components/map/map.test.tsx b/src/renderer/components/map/map.test.tsx new file mode 100644 index 0000000000..4171f1a1c1 --- /dev/null +++ b/src/renderer/components/map/map.test.tsx @@ -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( + ( +
Some placeholder
+ )} + > + {() =>
Irrelevant
} +
, + ); + }); + + 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( + + {() =>
Irrelevant
} +
, + ); + }); + + 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( + ( +
Some placeholder
+ )} + > + {(item) =>
} + , + ); + }); + + 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( +
Some separator
} + > + {(item) =>
} + , + ); + }); + + 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); + }); + }); +}); diff --git a/src/renderer/components/map/map.tsx b/src/renderer/components/map/map.tsx new file mode 100644 index 0000000000..ad33963002 --- /dev/null +++ b/src/renderer/components/map/map.tsx @@ -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 { + items: Item[]; + children: (item: Item) => React.ReactElement; + getPlaceholder?: () => React.ReactElement; + getSeparator?: () => React.ReactElement; +} + +export const Map = ( + props: MapProps, +) => { + 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 }) => ( + {render()} + )), + )} + + ); +};