[App Search] Added a ReorderableTable component (#106306)

This commit is contained in:
Jason Stoltzfus 2021-07-28 10:16:43 -04:00 committed by GitHub
parent 5575b937c8
commit 904c370378
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1006 additions and 1 deletions

View file

@ -19,12 +19,18 @@ import {
EuiDroppable,
EuiDraggable,
EuiButtonIconColor,
EuiEmptyPrompt,
} from '@elastic/eui';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { Schema, SchemaType } from '../../../shared/schema/types';
import { ReorderableTable } from '../../../shared/tables/reorderable_table';
import { Result } from '../result';
const NO_ITEMS = (
<EuiEmptyPrompt iconType="clock" title={<h2>No Items</h2>} body={<p>No Items</p>} />
);
export const Library: React.FC = () => {
const props = {
isMetaEngine: false,
@ -249,13 +255,108 @@ export const Library: React.FC = () => {
</EuiDragDropContext>
<EuiSpacer />
<EuiSpacer />
<EuiTitle size="s">
<h3>With field value type highlights</h3>
</EuiTitle>
<EuiSpacer />
<Result {...props} schemaForTypeHighlights={schema} />
<EuiSpacer />
<EuiTitle size="m">
<h2>ReorderableTable</h2>
</EuiTitle>
<EuiSpacer />
<ReorderableTable
noItemsMessage={NO_ITEMS}
items={[{ id: 1 }, { id: 2 }, { id: 3 }]}
columns={[
{ name: 'ID', render: (item) => <div>{item.id}</div> },
{ name: 'Whatever', render: (item) => <div>Whatever</div> },
]}
/>
<EuiSpacer />
<EuiTitle size="s">
<h3>With reordering disabled</h3>
</EuiTitle>
<EuiSpacer />
<ReorderableTable
disableReordering
noItemsMessage={NO_ITEMS}
items={[{ id: 1 }, { id: 2 }, { id: 3 }]}
columns={[
{ name: 'ID', render: (item) => <div>{item.id}</div> },
{ name: 'Whatever', render: (item) => <div>Whatever</div> },
]}
/>
<EuiSpacer />
<EuiTitle size="s">
<h3>With reordering enabled, but dragging disabled</h3>
</EuiTitle>
<EuiSpacer />
<ReorderableTable
disableDragging
noItemsMessage={NO_ITEMS}
items={[{ id: 1 }, { id: 2 }, { id: 3 }]}
columns={[
{ name: 'ID', render: (item) => <div>{item.id}</div> },
{ name: 'Whatever', render: (item) => <div>Whatever</div> },
]}
/>
<EuiSpacer />
<EuiTitle size="s">
<h3>With unreorderable items</h3>
</EuiTitle>
<EuiSpacer />
<ReorderableTable
noItemsMessage={NO_ITEMS}
items={[{ id: 1 }, { id: 2 }, { id: 3 }]}
unreorderableItems={[{ id: 4 }, { id: 5 }]}
columns={[
{ name: 'ID', render: (item) => <div>{item.id}</div> },
{ name: 'Whatever', render: (item) => <div>Whatever</div> },
]}
/>
<EuiSpacer />
<EuiTitle size="s">
<h3>Using the rowProps prop to apply dynamic properties to each row</h3>
</EuiTitle>
<EuiSpacer />
<ReorderableTable
rowProps={(item) => ({
style: {
backgroundColor: item.id % 2 === 0 ? 'red' : 'green',
},
})}
noItemsMessage={NO_ITEMS}
items={[{ id: 1 }, { id: 2 }, { id: 3 }]}
columns={[
{ name: 'ID', render: (item) => <div>{item.id}</div> },
{ name: 'Whatever', render: (item) => <div>Whatever</div> },
]}
/>
<EuiSpacer />
<EuiTitle size="s">
<h3>With no items</h3>
</EuiTitle>
<EuiSpacer />
<ReorderableTable
noItemsMessage={NO_ITEMS}
items={[]}
columns={[
{ name: 'ID', render: (item: { id: number }) => <div>{item.id}</div> },
{ name: 'Whatever', render: (item) => <div>Whatever</div> },
]}
/>
<EuiSpacer />
<EuiSpacer />
<EuiSpacer />
</EuiPageContentBody>
</EuiPageContent>
</>

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { BodyRow } from './body_row';
import { Cell } from './cell';
interface Foo {
id: number;
}
describe('BodyRow', () => {
const columns = [
{
name: 'ID',
flexBasis: 'foo',
flexGrow: 0,
alignItems: 'bar',
render: (item: Foo) => item.id,
},
{
name: 'Whatever',
render: () => 'Whatever',
},
];
const item = { id: 1 };
it('renders a table row from the provided item and columns', () => {
const wrapper = shallow(<BodyRow columns={columns} item={item} />);
const cells = wrapper.find(Cell);
expect(cells.length).toBe(2);
expect(cells.at(0).props()).toEqual({
alignItems: 'bar',
children: 1,
flexBasis: 'foo',
flexGrow: 0,
});
expect(cells.at(1).props()).toEqual({
children: 'Whatever',
});
});
it('will accept additional properties to apply to this row', () => {
const wrapper = shallow(
<BodyRow
columns={columns}
item={item}
additionalProps={{
className: 'some_class_name',
}}
/>
);
expect(wrapper.find('[data-test-subj="row"]').hasClass('some_class_name')).toBe(true);
});
it('will render an additional cell in the first column if one is provided', () => {
const wrapper = shallow(
<BodyRow columns={columns} item={item} leftAction={<div>Left Action</div>} />
);
const cells = wrapper.find(Cell);
expect(cells.length).toBe(3);
});
});

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Cell } from './cell';
import { DRAGGABLE_UX_STYLE } from './constants';
import { Column } from './types';
export interface BodyRowProps<Item> {
columns: Array<Column<Item>>;
item: Item;
additionalProps?: object;
// Cell to put in first column before other columns
leftAction?: React.ReactNode;
}
export const BodyRow = <Item extends object>({
columns,
item,
additionalProps,
leftAction,
}: BodyRowProps<Item>) => {
return (
<div className="reorderableTableRow">
<EuiFlexGroup data-test-subj="row" alignItems="center" {...(additionalProps || {})}>
<EuiFlexItem>
<EuiFlexGroup alignItems="flexStart">
{!!leftAction && <Cell {...DRAGGABLE_UX_STYLE}>{leftAction}</Cell>}
{columns.map((column, columnIndex) => (
<Cell
key={`table_row_cell_${columnIndex}`}
alignItems={column.alignItems}
flexBasis={column.flexBasis}
flexGrow={column.flexGrow}
>
{column.render(item)}
</Cell>
))}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
};

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { BodyRows } from './body_rows';
const Row = <Item extends object>({ item, itemIndex }: { item: Item; itemIndex: number }) => (
<div>
{item} {itemIndex}
</div>
);
describe('BodyRows', () => {
it('renders a row for each provided item', () => {
const wrapper = shallow(
<BodyRows
items={[{ id: 1 }, { id: 2 }]}
renderItem={(item, itemIndex) => <Row item={item} itemIndex={itemIndex} key={itemIndex} />}
/>
);
const rows = wrapper.find(Row);
expect(rows.length).toBe(2);
expect(rows.at(0).props()).toEqual({
itemIndex: 0,
item: { id: 1 },
});
expect(rows.at(1).props()).toEqual({
itemIndex: 1,
item: { id: 2 },
});
});
});

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
export interface BodyRowsProps<Item> {
items: Item[];
renderItem: (item: Item, itemIndex: number) => React.ReactNode;
}
export const BodyRows = <Item extends object>({ items, renderItem }: BodyRowsProps<Item>) => (
<>{items.map(renderItem)}</>
);

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { Cell } from './cell';
describe('Cell', () => {
it('renders a table cell with the provided content and styles', () => {
const wrapper = shallow(
<Cell flexBasis="foo" flexGrow={0} alignItems="bar">
Content
</Cell>
);
expect(wrapper.props()).toEqual({
style: {
flexBasis: 'foo',
flexGrow: 0,
alignItems: 'bar',
},
children: 'Content',
});
});
});

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiFlexItem } from '@elastic/eui';
import { DraggableUXStyles } from './types';
type CellProps<Item> = DraggableUXStyles;
export const Cell = <Item extends object>({
children,
alignItems,
flexBasis,
flexGrow,
}: CellProps<Item> & { children?: React.ReactNode }) => {
return (
<EuiFlexItem
style={{
flexBasis,
flexGrow,
alignItems,
}}
>
{children}
</EuiFlexItem>
);
};

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DraggableUXStyles } from './types';
export const DRAGGABLE_UX_STYLE: DraggableUXStyles = {
flexBasis: '16px',
flexGrow: 0,
alignItems: 'center',
};

View file

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { EuiDraggable, EuiIcon } from '@elastic/eui';
import { BodyRow } from './body_row';
import { DraggableBodyRow } from './draggable_body_row';
interface Foo {
id: number;
}
describe('DraggableBodyRow', () => {
const columns = [
{
name: 'ID',
flexBasis: 'foo',
flexGrow: 0,
alignItems: 'bar',
render: (item: Foo) => item.id,
},
{
name: 'Whatever',
render: () => 'Whatever',
},
];
const item = { id: 1 };
const additionalProps = {};
it('wraps a BodyRow with an EuiDraggable and injects a drag handle as the first cell', () => {
const wrapper = shallow(
<DraggableBodyRow
columns={columns}
item={item}
rowIndex={1}
additionalProps={additionalProps}
/>
);
const euiDraggable = wrapper.find(EuiDraggable);
expect(euiDraggable.exists()).toBe(true);
// It adds an index and unique draggable id from the provided rowIndex
expect(euiDraggable.prop('index')).toEqual(1);
expect(euiDraggable.prop('draggableId')).toEqual('draggable_row_1');
const bodyRow = wrapper.find(BodyRow);
expect(bodyRow.exists()).toEqual(true);
expect(bodyRow.props()).toEqual(expect.objectContaining({ columns, item, additionalProps }));
const leftAction = shallow(<div>{bodyRow.prop('leftAction')}</div>);
expect(leftAction.find(EuiIcon).exists()).toBe(true);
});
it('will accept a parameter that disables dragging', () => {
const wrapper = shallow(
<DraggableBodyRow columns={columns} item={item} rowIndex={1} disableDragging />
);
const euiDraggable = wrapper.find(EuiDraggable);
expect(euiDraggable.prop('isDragDisabled')).toBe(true);
expect(euiDraggable.prop('disableInteractiveElementBlocking')).toBe(true);
});
});

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiDraggable, EuiIcon } from '@elastic/eui';
import { BodyRow } from './body_row';
import { Column } from './types';
export interface DraggableBodyRowProps<Item> {
columns: Array<Column<Item>>;
item: Item;
rowIndex: number;
additionalProps?: object;
disableDragging?: boolean;
}
export const DraggableBodyRow = <Item extends object>({
columns,
item,
rowIndex,
additionalProps,
disableDragging = false,
}: DraggableBodyRowProps<Item>) => {
const draggableId = `draggable_row_${rowIndex}`;
return (
<EuiDraggable
index={rowIndex}
draggableId={draggableId}
isDragDisabled={disableDragging}
disableInteractiveElementBlocking={disableDragging}
{...additionalProps}
>
<BodyRow
columns={columns}
item={item}
additionalProps={additionalProps}
leftAction={!disableDragging ? <EuiIcon type="grab" /> : <></>}
/>
</EuiDraggable>
);
};

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { EuiDragDropContext } from '@elastic/eui';
import { BodyRows } from './body_rows';
import { DraggableBodyRows } from './draggable_body_rows';
describe('DraggableBodyRows', () => {
const items = [{ id: 1 }, { id: 2 }];
const onReorder = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('wraps BodyRows in a EuiDragDropContext', () => {
const renderItem = jest.fn();
const wrapper = shallow(
<DraggableBodyRows items={items} onReorder={onReorder} renderItem={renderItem} />
);
expect(wrapper.find(EuiDragDropContext).exists()).toBe(true);
const bodyRows = wrapper.find(BodyRows);
expect(bodyRows.props()).toEqual({
items,
renderItem,
});
});
it('will call the provided onReorder function whenever items are reordered', () => {
const wrapper = shallow(
<DraggableBodyRows items={items} onReorder={onReorder} renderItem={jest.fn()} />
);
wrapper.find(EuiDragDropContext).simulate('dragEnd', {
source: { index: 1 },
destination: { index: 0 },
});
expect(onReorder).toHaveBeenCalledWith([{ id: 2 }, { id: 1 }], items);
});
it('will not call the provided onReorder function if there are not a source AND destination provided', () => {
const wrapper = shallow(
<DraggableBodyRows items={items} onReorder={onReorder} renderItem={jest.fn()} />
);
wrapper.find(EuiDragDropContext).simulate('dragEnd', {
source: { index: 1 },
});
expect(onReorder).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiDragDropContext, euiDragDropReorder, EuiDroppable } from '@elastic/eui';
import { BodyRows } from './body_rows';
interface DraggableBodyRowsProps<Item> {
items: Item[];
onReorder: (reorderedItems: Item[], items: Item[]) => void;
renderItem: (item: Item, itemIndex: number) => React.ReactNode;
}
export const DraggableBodyRows = <Item extends object>({
items,
onReorder,
renderItem,
}: DraggableBodyRowsProps<Item>) => {
return (
<EuiDragDropContext
onDragEnd={({ source, destination }) => {
if (source && destination) {
const reorderedItems = euiDragDropReorder(items, source.index, destination?.index);
onReorder(reorderedItems, items);
}
}}
>
<EuiDroppable droppableId="ReorderingArea" grow={false}>
<BodyRows items={items} renderItem={renderItem} />
</EuiDroppable>
</EuiDragDropContext>
);
};

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { Cell } from './cell';
import { HeaderRow } from './header_row';
interface Foo {
id: number;
}
describe('HeaderRow', () => {
const columns = [
{ name: 'ID', render: (item: Foo) => <div>{item.id}</div> },
{ name: 'Whatever', render: () => 'Whatever' },
];
it('renders a table header row from the provided column names', () => {
const wrapper = shallow(<HeaderRow columns={columns} />);
const cells = wrapper.find(Cell);
expect(cells.length).toBe(2);
expect(cells.at(0).children().dive().text()).toEqual('ID');
expect(cells.at(1).children().dive().text()).toEqual('Whatever');
});
it('will render an additional cell in the first column if one is provided', () => {
const wrapper = shallow(<HeaderRow columns={columns} leftAction={<div>Left Action</div>} />);
const cells = wrapper.find(Cell);
expect(cells.length).toBe(3);
});
});

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { Cell } from './cell';
import { DRAGGABLE_UX_STYLE } from './constants';
import { Column } from './types';
interface HeaderRowProps<Item> {
columns: Array<Column<Item>>;
// Cell to put in first column before other columns
leftAction?: React.ReactNode;
}
export const HeaderRow = <Item extends object>({ columns, leftAction }: HeaderRowProps<Item>) => {
return (
<div className="reorderableTableHeader">
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup>
{!!leftAction && <Cell {...DRAGGABLE_UX_STYLE}>{leftAction}</Cell>}
{columns.map((column, columnIndex) => (
<Cell key={`table_header_cell_${columnIndex}`} {...column}>
<EuiText size="s">
<strong>{column.name}</strong>
</EuiText>
</Cell>
))}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
};

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { ReorderableTable } from './reorderable_table';

View file

@ -0,0 +1,26 @@
.reorderableTable {
&NoItems {
border-top: $euiBorderThin;
background-color: $euiColorEmptyShade;
}
&Row {
border-top: $euiBorderThin;
background-color: $euiColorEmptyShade;
> .euiFlexGroup--directionRow.euiFlexGroup--gutterLarge {
margin: 0;
}
}
&Header {
> .euiFlexGroup--directionRow.euiFlexGroup--gutterLarge {
margin: -12px 0;
}
}
.euiDraggable .euiDraggable__item.euiDraggable__item--isDisabled {
cursor: unset;
}
}

View file

@ -0,0 +1,191 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { BodyRow } from './body_row';
import { BodyRows } from './body_rows';
import { DraggableBodyRow } from './draggable_body_row';
import { DraggableBodyRows } from './draggable_body_rows';
import { HeaderRow } from './header_row';
import { ReorderableTable } from './reorderable_table';
import { Column } from './types';
interface Foo {
id: number;
}
describe('ReorderableTable', () => {
const items: Foo[] = [{ id: 1 }, { id: 2 }];
const columns: Array<Column<Foo>> = [];
describe('when the table is reorderable', () => {
it('renders with a header that has an additional column injected as the first column, which is empty', () => {
const wrapper = shallow(
<ReorderableTable noItemsMessage={<p>No Items</p>} items={items} columns={columns} />
);
const header = wrapper.find(HeaderRow);
expect(header.exists()).toEqual(true);
expect(header.prop('columns')).toEqual(columns);
expect(header.prop('leftAction')).not.toBeUndefined();
});
it('renders draggable rows inside of the reorderable table', () => {
const wrapper = shallow(
<ReorderableTable noItemsMessage={<p>No Items</p>} items={items} columns={columns} />
);
const bodyRows = wrapper.find(DraggableBodyRows);
expect(bodyRows.exists()).toBe(true);
expect(bodyRows.prop('items')).toEqual(items);
const renderedRow = bodyRows.renderProp('renderItem')({ id: 1 }, 0);
expect(renderedRow.type()).toEqual(DraggableBodyRow);
expect(renderedRow.props()).toEqual({
columns,
item: { id: 1 },
additionalProps: {},
disableDragging: false,
rowIndex: 0,
});
});
it('can append additional properties to each row, which can be dynamically calculated from the item in that row', () => {
const wrapper = shallow(
<ReorderableTable
noItemsMessage={<p>No Items</p>}
items={items}
columns={columns}
rowProps={(item) => ({
className: `someClassName_${item.id}`,
})}
/>
);
const renderedRow = wrapper.find(DraggableBodyRows).renderProp('renderItem')({ id: 1 }, 0);
expect(renderedRow.prop('additionalProps')).toEqual({
className: 'someClassName_1',
});
});
it('will disableDragging on individual rows if disableDragging is enabled', () => {
const wrapper = shallow(
<ReorderableTable
noItemsMessage={<p>No Items</p>}
items={items}
columns={columns}
disableDragging
/>
);
const renderedRow = wrapper.find(DraggableBodyRows).renderProp('renderItem')({ id: 1 }, 0);
expect(renderedRow.prop('disableDragging')).toEqual(true);
});
it('will accept a callback which will be triggered every time a row is reordered', () => {
const onReorder = jest.fn();
const wrapper = shallow(
<ReorderableTable
noItemsMessage={<p>No Items</p>}
items={items}
columns={columns}
onReorder={onReorder}
/>
);
expect(wrapper.find(DraggableBodyRows).prop('onReorder')).toEqual(onReorder);
});
it('will provide a default callback for reordered if none is provided, which does nothing', () => {
const wrapper = shallow(
<ReorderableTable noItemsMessage={<p>No Items</p>} items={items} columns={columns} />
);
const onReorder = wrapper.find(DraggableBodyRows).prop('onReorder');
expect(onReorder([], [])).toBeUndefined();
});
it('will render items that cant be reordered', () => {
const unreorderableItems = [{ id: 3 }];
const wrapper = shallow(
<ReorderableTable
noItemsMessage={<p>No Items</p>}
items={items}
unreorderableItems={unreorderableItems}
columns={columns}
/>
);
const bodyRows = wrapper.find(BodyRows);
expect(bodyRows.exists()).toBe(true);
expect(bodyRows.prop('items')).toEqual(unreorderableItems);
const renderedRow = bodyRows.renderProp('renderItem')({ id: 1 }, 0);
expect(renderedRow.type()).toEqual(BodyRow);
expect(renderedRow.props()).toEqual({
columns,
item: { id: 1 },
additionalProps: {},
leftAction: expect.anything(),
});
});
});
describe('when reorderable is turned off on the table', () => {
it('renders a table with a header and non-reorderable rows', () => {
const wrapper = shallow(
<ReorderableTable
noItemsMessage={<p>No Items</p>}
items={items}
columns={columns}
disableReordering
/>
);
const bodyRows = wrapper.find(BodyRows);
expect(bodyRows.exists()).toBe(true);
expect(bodyRows.prop('items')).toEqual(items);
const renderedRow = bodyRows.renderProp('renderItem')({ id: 1 }, 0);
expect(renderedRow.type()).toEqual(BodyRow);
expect(renderedRow.props()).toEqual({
columns,
item: { id: 1 },
additionalProps: {},
});
});
it('can append additional properties to each row, which can be dynamically calculated from the item in that row', () => {
const wrapper = shallow(
<ReorderableTable
noItemsMessage={<p>No Items</p>}
items={items}
columns={columns}
rowProps={(item) => ({
className: `someClassName_${item.id}`,
})}
disableReordering
/>
);
const renderedRow = wrapper.find(BodyRows).renderProp('renderItem')({ id: 1 }, 0);
expect(renderedRow.prop('additionalProps')).toEqual({
className: 'someClassName_1',
});
});
});
it('appends an additional className if specified', () => {
const wrapper = shallow(
<ReorderableTable noItemsMessage={<p>No Items</p>} items={[]} columns={[]} className="foo" />
);
expect(wrapper.hasClass('foo')).toBe(true);
});
it('will show a no items message when there are no items', () => {
const wrapper = shallow(
<ReorderableTable noItemsMessage={<p>No Items</p>} items={[]} columns={columns} />
);
expect(wrapper.find('[data-test-subj="NoItems"]').exists()).toBe(true);
expect(wrapper.find(BodyRows).exists()).toBe(false);
expect(wrapper.find(DraggableBodyRows).exists()).toBe(false);
});
});

View file

@ -0,0 +1,106 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import classNames from 'classnames';
import './reorderable_table.scss';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { BodyRow } from './body_row';
import { BodyRows } from './body_rows';
import { DraggableBodyRow } from './draggable_body_row';
import { DraggableBodyRows } from './draggable_body_rows';
import { HeaderRow } from './header_row';
import { Column } from './types';
interface ReorderableTableProps<Item> {
columns: Array<Column<Item>>;
items: Item[];
noItemsMessage: React.ReactNode;
unreorderableItems?: Item[];
className?: string;
disableDragging?: boolean;
disableReordering?: boolean;
onReorder?: (items: Item[], oldItems: Item[]) => void;
rowProps?: (item: Item) => object;
}
export const ReorderableTable = <Item extends object>({
columns,
items,
noItemsMessage,
unreorderableItems = [],
className = '',
disableDragging = false,
disableReordering = false,
onReorder = () => undefined,
rowProps = () => ({}),
}: ReorderableTableProps<Item>) => {
return (
<div className={classNames(className, 'reorderableTable')}>
<HeaderRow columns={columns} leftAction={!disableReordering ? <></> : undefined} />
{items.length === 0 && (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem data-test-subj="NoItems" className="reorderableTableNoItems">
{noItemsMessage}
</EuiFlexItem>
</EuiFlexGroup>
)}
{items.length > 0 && !disableReordering && (
<>
<DraggableBodyRows
items={items}
renderItem={(item, itemIndex) => (
<DraggableBodyRow
key={`table_draggable_row_${itemIndex}`}
columns={columns}
item={item}
additionalProps={rowProps(item)}
disableDragging={disableDragging}
rowIndex={itemIndex}
/>
)}
onReorder={onReorder}
/>
{unreorderableItems.length > 0 && (
<BodyRows
items={unreorderableItems}
renderItem={(item, itemIndex) => (
<BodyRow
key={`table_draggable_row_${itemIndex}`}
columns={columns}
item={item}
additionalProps={rowProps(item)}
leftAction={<></>}
/>
)}
/>
)}
</>
)}
{items.length > 0 && disableReordering && (
<BodyRows
items={items}
renderItem={(item, itemIndex) => (
<BodyRow
key={`table_draggable_row_${itemIndex}`}
columns={columns}
item={item}
additionalProps={rowProps(item)}
/>
)}
/>
)}
</div>
);
};

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface DraggableUXStyles {
alignItems?: string;
flexBasis?: string;
flexGrow?: number;
}
export interface Column<Item> extends DraggableUXStyles {
name: string;
render: (item: Item) => React.ReactNode;
}