Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Enterprise Search] Added the InlineEditableTable component #107208

Merged
merged 16 commits into from
Jul 29, 2021
Merged
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