`;
+
+exports[`Storyshots Table|Table Default 1`] = `
+
+`;
+
+exports[`Storyshots Table|Table TextColumn 1`] = `
+
+
+
+ |
+ Id
+
+ |
+
+ Name
+
+ |
+
+ Description
+
+ |
+
+
+
+
+ |
+ 21
+ |
+
+ Pommes
+ |
+
+ Fried potato sticks
+ |
+
+
+ |
+ 42
+ |
+
+ Quarter-Pounder
+ |
+
+ Big burger
+ |
+
+
+ |
+ -84
+ |
+
+ Icecream
+ |
+
+ Cold dessert
+ |
+
+
+
+`;
diff --git a/scm-ui/ui-components/src/index.ts b/scm-ui/ui-components/src/index.ts
index f5c7f62418..d37a04b2e5 100644
--- a/scm-ui/ui-components/src/index.ts
+++ b/scm-ui/ui-components/src/index.ts
@@ -63,6 +63,7 @@ export * from "./layout";
export * from "./modals";
export * from "./navigation";
export * from "./repos";
+export * from "./table";
export {
File,
diff --git a/scm-ui/ui-components/src/table/Column.tsx b/scm-ui/ui-components/src/table/Column.tsx
new file mode 100644
index 0000000000..4cd0f5d30d
--- /dev/null
+++ b/scm-ui/ui-components/src/table/Column.tsx
@@ -0,0 +1,18 @@
+import React, { FC, ReactNode } from "react";
+import { ColumnProps } from "./table";
+
+type Props = ColumnProps & {
+ children: (row: any, columnIndex: number) => ReactNode;
+};
+
+const Column: FC
= ({ row, columnIndex, children }) => {
+ if (row === undefined) {
+ throw new Error("missing row, use column only as child of Table");
+ }
+ if (columnIndex === undefined) {
+ throw new Error("missing row, use column only as child of Table");
+ }
+ return <>{children(row, columnIndex)}>;
+};
+
+export default Column;
diff --git a/scm-ui/ui-components/src/table/SortIcon.tsx b/scm-ui/ui-components/src/table/SortIcon.tsx
new file mode 100644
index 0000000000..0d553041c6
--- /dev/null
+++ b/scm-ui/ui-components/src/table/SortIcon.tsx
@@ -0,0 +1,19 @@
+import React, { FC } from "react";
+import styled from "styled-components";
+import Icon from "../Icon";
+
+type Props = {
+ name: string;
+ isVisible: boolean;
+};
+
+const IconWithMarginLeft = styled(Icon)`
+ visibility: ${(props: Props) => (props.isVisible ? "visible" : "hidden")};
+ margin-left: 0.25em;
+`;
+
+const SortIcon: FC = (props: Props) => {
+ return ;
+};
+
+export default SortIcon;
diff --git a/scm-ui/ui-components/src/table/Table.stories.tsx b/scm-ui/ui-components/src/table/Table.stories.tsx
new file mode 100644
index 0000000000..b4df2a1cb8
--- /dev/null
+++ b/scm-ui/ui-components/src/table/Table.stories.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import Table from "./Table";
+import Column from "./Column";
+import TextColumn from "./TextColumn";
+
+storiesOf("Table|Table", module)
+ .add("Default", () => (
+
+ {(row: any) => {row.firstname}
}
+ {
+ return (a: any, b: any) => {
+ if (a.lastname > b.lastname) {
+ return -1;
+ } else if (a.lastname < b.lastname) {
+ return 1;
+ } else {
+ return 0;
+ }
+ };
+ }}
+ >
+ {(row: any) => {row.lastname}}
+
+ {(row: any) => {row.email}}
+
+ ))
+ .add("TextColumn", () => (
+
+ ))
+ .add("Empty", () => (
+
+ ));
diff --git a/scm-ui/ui-components/src/table/Table.tsx b/scm-ui/ui-components/src/table/Table.tsx
new file mode 100644
index 0000000000..ffcd38238f
--- /dev/null
+++ b/scm-ui/ui-components/src/table/Table.tsx
@@ -0,0 +1,117 @@
+import React, { FC, ReactElement, useState } from "react";
+import styled from "styled-components";
+import { Comparator } from "./table";
+import SortIcon from "./SortIcon";
+import Notification from "../Notification";
+
+const StyledTable = styled.table.attrs(() => ({
+ className: "table content is-hoverable"
+}))``;
+
+type Props = {
+ data: any[];
+ sortable?: boolean;
+ emptyMessage?: string;
+ children: Array;
+};
+
+const Table: FC = ({ data, sortable, children, emptyMessage }) => {
+ const [tableData, setTableData] = useState(data);
+ const [ascending, setAscending] = useState(false);
+ const [lastSortBy, setlastSortBy] = useState();
+ const [hoveredColumnIndex, setHoveredColumnIndex] = useState();
+
+ const isSortable = (child: ReactElement) => {
+ return sortable && child.props.createComparator;
+ };
+
+ const sortFunctions: Comparator | undefined[] = [];
+ React.Children.forEach(children, (child, index) => {
+ if (child && isSortable(child)) {
+ sortFunctions.push(child.props.createComparator(child.props, index));
+ } else {
+ sortFunctions.push(undefined);
+ }
+ });
+
+ const mapDataToColumns = (row: any) => {
+ return (
+
+ {React.Children.map(children, (child, columnIndex) => {
+ return | {React.cloneElement(child, { ...child.props, columnIndex, row })} | ;
+ })}
+
+ );
+ };
+
+ const sortDescending = (sortAscending: (a: any, b: any) => number) => {
+ return (a: any, b: any) => {
+ return sortAscending(a, b) * -1;
+ };
+ };
+
+ const tableSort = (index: number) => {
+ const sortFn = sortFunctions[index];
+ if (!sortFn) {
+ throw new Error(`column with index ${index} is not sortable`);
+ }
+ const sortableData = [...tableData];
+ let sortOrder = ascending;
+ if (lastSortBy !== index) {
+ setAscending(true);
+ sortOrder = true;
+ }
+ const sortFunction = sortOrder ? sortFn : sortDescending(sortFn);
+ sortableData.sort(sortFunction);
+ setTableData(sortableData);
+ setAscending(!sortOrder);
+ setlastSortBy(index);
+ };
+
+ const shouldShowIcon = (index: number) => {
+ return index === lastSortBy || index === hoveredColumnIndex;
+ };
+
+ if (!tableData || tableData.length <= 0) {
+ if (emptyMessage) {
+ return {emptyMessage};
+ } else {
+ return null;
+ }
+ }
+
+ return (
+
+
+
+ {React.Children.map(children, (child, index) => (
+ | tableSort(index) : undefined}
+ onMouseEnter={() => setHoveredColumnIndex(index)}
+ onMouseLeave={() => setHoveredColumnIndex(undefined)}
+ >
+ {child.props.header}
+ {isSortable(child) && renderSortIcon(child, ascending, shouldShowIcon(index))}
+ |
+ ))}
+
+
+ {tableData.map(mapDataToColumns)}
+
+ );
+};
+
+Table.defaultProps = {
+ sortable: true
+};
+
+const renderSortIcon = (child: ReactElement, ascending: boolean, showIcon: boolean) => {
+ if (child.props.ascendingIcon && child.props.descendingIcon) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+export default Table;
diff --git a/scm-ui/ui-components/src/table/TextColumn.tsx b/scm-ui/ui-components/src/table/TextColumn.tsx
new file mode 100644
index 0000000000..a170b02192
--- /dev/null
+++ b/scm-ui/ui-components/src/table/TextColumn.tsx
@@ -0,0 +1,28 @@
+import React, { FC } from "react";
+import { ColumnProps } from "./table";
+
+type Props = ColumnProps & {
+ dataKey: string;
+};
+
+const TextColumn: FC = ({ row, dataKey }) => {
+ return row[dataKey];
+};
+
+TextColumn.defaultProps = {
+ createComparator: (props: Props) => {
+ return (a: any, b: any) => {
+ if (a[props.dataKey] < b[props.dataKey]) {
+ return -1;
+ } else if (a[props.dataKey] > b[props.dataKey]) {
+ return 1;
+ } else {
+ return 0;
+ }
+ };
+ },
+ ascendingIcon: "sort-alpha-down-alt",
+ descendingIcon: "sort-alpha-down"
+};
+
+export default TextColumn;
diff --git a/scm-ui/ui-components/src/table/index.ts b/scm-ui/ui-components/src/table/index.ts
new file mode 100644
index 0000000000..c99a4e64c1
--- /dev/null
+++ b/scm-ui/ui-components/src/table/index.ts
@@ -0,0 +1,4 @@
+export { default as Table } from "./Table";
+export { default as Column } from "./Column";
+export { default as TextColumn } from "./TextColumn";
+export { default as SortIcon } from "./SortIcon";
diff --git a/scm-ui/ui-components/src/table/table.ts b/scm-ui/ui-components/src/table/table.ts
new file mode 100644
index 0000000000..9af3e872cb
--- /dev/null
+++ b/scm-ui/ui-components/src/table/table.ts
@@ -0,0 +1,12 @@
+import { ReactNode } from "react";
+
+export type Comparator = (a: any, b: any) => number;
+
+export type ColumnProps = {
+ header: ReactNode;
+ row?: any;
+ columnIndex?: number;
+ createComparator?: (props: any, columnIndex: number) => Comparator;
+ ascendingIcon?: string;
+ descendingIcon?: string;
+};