[App Search] Added a ReorderableTable component (#106306)
This commit is contained in:
parent
5575b937c8
commit
904c370378
19 changed files with 1006 additions and 1 deletions
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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)}</>
|
||||
);
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue