Skip to content

Commit

Permalink
[Enterprise Search] Added the InlineEditableTable component (#107208) (
Browse files Browse the repository at this point in the history
…#107247)

Co-authored-by: Jason Stoltzfus <jastoltz24@gmail.com>
  • Loading branch information
kibanamachine and JasonStoltz committed Jul 30, 2021
1 parent d6abb09 commit 8f63231
Show file tree
Hide file tree
Showing 16 changed files with 1,871 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,24 @@ import {

import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { Schema, SchemaType } from '../../../shared/schema/types';
import { InlineEditableTable } from '../../../shared/tables/inline_editable_table';
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>} />
);

// For the InlineEditableTable
// Since InlineEditableTable caches handlers, we need to store this globally somewhere rather than just in useState
interface Foo {
id: number;
foo: string;
bar: string;
}
let globalItems: Foo[] = [];
const getLastItems = () => globalItems;

export const Library: React.FC = () => {
const props = {
isMetaEngine: false,
Expand Down Expand Up @@ -91,6 +102,67 @@ export const Library: React.FC = () => {
},
];

// For the InlineEditableTable
const [items, setItems] = useState([
{ id: 1, foo: 'foo1', bar: '10' },
{ id: 2, foo: 'foo2', bar: '10' },
]);
globalItems = items;
const columns = [
{
field: 'foo',
name: 'Foo',
render: (item: Foo) => <div>{item.foo}</div>,
editingRender: (item: Foo, onChange: (value: string) => void) => (
<input type="text" value={item.foo} onChange={(e) => onChange(e.target.value)} />
),
},
{
field: 'bar',
name: 'Bar (Must be a number)',
render: (item: Foo) => <div>{item.bar}</div>,
editingRender: (item: Foo, onChange: (value: string) => void) => (
<input type="text" value={item.bar} onChange={(e) => onChange(e.target.value)} />
),
},
];
const onAdd = (item: Foo, onSuccess: () => void) => {
const highestId = Math.max(...getLastItems().map((i) => i.id));
setItems([
...getLastItems(),
{
...item,
id: highestId + 1,
},
]);
onSuccess();
};
const onDelete = (item: Foo) => {
setItems(getLastItems().filter((i) => i.id !== item.id));
};
const onUpdate = (item: Foo, onSuccess: () => void) => {
setItems(
getLastItems().map((i) => {
if (item.id === i.id) return item;
return i;
})
);
onSuccess();
};
const validateItem = (item: Foo) => {
let isValidNumber = false;
const num = parseInt(item.bar, 10);
if (!isNaN(num)) isValidNumber = true;

if (isValidNumber) return {};
return {
bar: 'Bar must be a valid number',
};
};
const onReorder = (newItems: Foo[]) => {
setItems(newItems);
};

return (
<>
<SetPageChrome trail={['Library']} />
Expand Down Expand Up @@ -353,8 +425,99 @@ export const Library: React.FC = () => {
{ name: 'Whatever', render: (item) => <div>Whatever</div> },
]}
/>
<EuiSpacer />

<EuiTitle size="m">
<h2>InlineEditableTable</h2>
</EuiTitle>
<EuiSpacer />

<EuiTitle size="s">
<h3>With uneditable items</h3>
</EuiTitle>
<EuiSpacer />
<InlineEditableTable
items={items}
uneditableItems={[{ id: 3, foo: 'foo', bar: 'bar' }]}
instanceId="MyInstance"
title="My table"
description="Some description"
columns={columns}
onAdd={onAdd}
onDelete={onDelete}
onUpdate={onUpdate}
validateItem={validateItem}
onReorder={onReorder}
/>
<EuiSpacer />

<EuiTitle size="s">
<h3>Can delete last item</h3>
</EuiTitle>
<EuiSpacer />
<InlineEditableTable
items={items}
instanceId="MyInstance1"
title="My table"
description="Some description"
columns={columns}
onAdd={onAdd}
onDelete={onDelete}
onUpdate={onUpdate}
validateItem={validateItem}
onReorder={onReorder}
canRemoveLastItem
noItemsMessage={(edit) => {
return (
<EuiEmptyPrompt
iconType="clock"
title={<h2>No Items</h2>}
body={<button onClick={edit}>Click to create one</button>}
/>
);
}}
/>
<EuiSpacer />

<EuiTitle size="s">
<h3>Cannot delete last item</h3>
</EuiTitle>
<EuiSpacer />
<InlineEditableTable
items={items}
instanceId="MyInstance2"
title="My table"
description="Some description"
columns={columns}
onAdd={onAdd}
onDelete={onDelete}
onUpdate={onUpdate}
validateItem={validateItem}
onReorder={onReorder}
canRemoveLastItem={false}
lastItemWarning="This is the last item, you cannot delete it!"
/>
<EuiSpacer />

<EuiTitle size="s">
<h3>When isLoading is true</h3>
</EuiTitle>
<EuiSpacer />
<InlineEditableTable
isLoading
items={items}
instanceId="MyInstance3"
title="My table"
description="Some description"
columns={columns}
onAdd={onAdd}
onDelete={onDelete}
onUpdate={onUpdate}
validateItem={validateItem}
onReorder={onReorder}
/>
<EuiSpacer />

<EuiSpacer />
<EuiSpacer />
</EuiPageContentBody>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* 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 { setMockActions, setMockValues } from '../../../__mocks__/kea_logic';

import React from 'react';

import { shallow, ShallowWrapper } from 'enzyme';

import { ActionColumn } from './action_column';

const requiredParams = {
displayedItems: [],
isActivelyEditing: () => false,
item: { id: 1 },
};

describe('ActionColumn', () => {
const mockValues = {
doesEditingItemValueContainEmptyProperty: false,
editingItemId: 1,
formErrors: [],
isEditing: false,
isEditingUnsavedItem: false,
};
const mockActions = {
editExistingItem: jest.fn(),
deleteItem: jest.fn(),
doneEditing: jest.fn(),
saveExistingItem: jest.fn(),
saveNewItem: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
setMockValues(mockValues);
setMockActions(mockActions);
});

it('renders', () => {
const wrapper = shallow(
<ActionColumn
displayedItems={[]}
isActivelyEditing={() => false}
isLoading={false}
item={{ id: 1 }}
canRemoveLastItem={false}
lastItemWarning="I am a warning"
uneditableItems={[]}
/>
);
expect(wrapper.isEmptyRender()).toBe(false);
});

it('renders nothing if the item is an uneditableItem', () => {
const item = { id: 1 };
const wrapper = shallow(
<ActionColumn
displayedItems={[]}
isActivelyEditing={() => false}
isLoading={false}
item={item}
canRemoveLastItem={false}
lastItemWarning="I am a warning"
uneditableItems={[item]}
/>
);
expect(wrapper.isEmptyRender()).toBe(true);
});

describe('when the user is actively editing', () => {
const isActivelyEditing = () => true;
const activelyEditingParams = {
...requiredParams,
isActivelyEditing,
};

describe('it renders a save button', () => {
const subject = (wrapper: ShallowWrapper) => wrapper.find('[data-test-subj="saveButton"]');

it('which is disabled if data is loading', () => {
const wrapper = shallow(<ActionColumn {...activelyEditingParams} isLoading />);
expect(subject(wrapper).prop('disabled')).toBe(true);
});

it('which is disabled if there are form errors', () => {
setMockValues({
...mockValues,
formErrors: ['I am an error'],
});

const wrapper = shallow(<ActionColumn {...activelyEditingParams} />);
expect(subject(wrapper).prop('disabled')).toBe(true);
});

it('which is disabled if the item value contains an empty property', () => {
setMockValues({
...mockValues,
doesEditingItemValueContainEmptyProperty: true,
});

const wrapper = shallow(<ActionColumn {...activelyEditingParams} />);
expect(subject(wrapper).prop('disabled')).toBe(true);
});

it('which calls saveNewItem when clicked if the user is editing an unsaved item', () => {
setMockValues({
...mockValues,
isEditingUnsavedItem: true,
});

const wrapper = shallow(<ActionColumn {...activelyEditingParams} />);
subject(wrapper).simulate('click');
expect(mockActions.saveNewItem).toHaveBeenCalled();
});

it('which calls saveExistingItem when clicked if the user is NOT editing an unsaved item', () => {
setMockValues({
...mockValues,
isEditingUnsavedItem: false,
});

const wrapper = shallow(<ActionColumn {...activelyEditingParams} />);
subject(wrapper).simulate('click');
expect(mockActions.saveExistingItem).toHaveBeenCalled();
});
});

describe('it renders a cancel button', () => {
const subject = (wrapper: ShallowWrapper) => wrapper.find('[data-test-subj="cancelButton"]');

it('which is disabled if data is loading', () => {
const wrapper = shallow(<ActionColumn {...activelyEditingParams} isLoading />);
expect(subject(wrapper).prop('disabled')).toBe(true);
});

it('which calls doneEditing when clicked', () => {
const wrapper = shallow(<ActionColumn {...activelyEditingParams} />);
subject(wrapper).simulate('click');
expect(mockActions.doneEditing).toHaveBeenCalled();
});
});
});

describe('when the user is NOT actively editing', () => {
const item = { id: 2 };

const mockValuesWhereUserIsNotActivelyEditing = {
...mockValues,
isEditing: false,
};

beforeEach(() => {
setMockValues(mockValuesWhereUserIsNotActivelyEditing);
});

describe('it renders an edit button', () => {
const subject = (wrapper: ShallowWrapper) => wrapper.find('[data-test-subj="editButton"]');

it('which calls editExistingItem when clicked', () => {
const wrapper = shallow(<ActionColumn {...requiredParams} item={item} />);
subject(wrapper).simulate('click');
expect(mockActions.editExistingItem).toHaveBeenCalledWith(item);
});
});

describe('it renders an delete button', () => {
const subject = (wrapper: ShallowWrapper) => wrapper.find('[data-test-subj="deleteButton"]');

it('which calls deleteItem when clicked', () => {
const wrapper = shallow(<ActionColumn {...requiredParams} item={item} />);
subject(wrapper).simulate('click');
expect(mockActions.deleteItem).toHaveBeenCalledWith(item);
});

it('which does not render if candRemoveLastItem is prevented and this is the last item', () => {
const wrapper = shallow(
<ActionColumn
{...requiredParams}
displayedItems={[item]}
item={item}
canRemoveLastItem={false}
/>
);
expect(subject(wrapper).exists()).toBe(false);
});
});
});
});
Loading

0 comments on commit 8f63231

Please sign in to comment.