Skip to content

Commit

Permalink
Migrate Table visualization to React Part 2: Editor (#4175)
Browse files Browse the repository at this point in the history
* Migrate table editor to React: skeleton, Grid tab

* Columns tab

* Cleanup

* Columns tab: DnD column sorting

* Columns types should be JSX

* New Columns tab UI/X

* Use Sortable component on Columns tab

* Tests: Grid Settings

* Tests: Columns Settings

* Tests: Editors for Text, Number, Boolean and Date/Time columns

* Tests: Editors for Image and Link columns

* Minor UI fix

* Trigger build

* Debounce inputs
  • Loading branch information
kravets-levko authored and arikfr committed Oct 24, 2019
1 parent 9f78446 commit 7157244
Show file tree
Hide file tree
Showing 40 changed files with 1,631 additions and 580 deletions.
104 changes: 104 additions & 0 deletions client/app/visualizations/table/Editor/ColumnEditor.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { map, keys } from 'lodash';
import React from 'react';
import { useDebouncedCallback } from 'use-debounce';
import PropTypes from 'prop-types';
import * as Grid from 'antd/lib/grid';
import Input from 'antd/lib/input';
import Radio from 'antd/lib/radio';
import Checkbox from 'antd/lib/checkbox';
import Select from 'antd/lib/select';
import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';

import ColumnTypes from '../columns';

export default function ColumnEditor({ column, onChange }) {
function handleChange(changes) {
onChange({ ...column, ...changes });
}

const [handleChangeDebounced] = useDebouncedCallback(handleChange, 200);

const AdditionalOptions = ColumnTypes[column.displayAs].Editor || null;

return (
<div className="table-visualization-editor-column">
<Grid.Row gutter={15} type="flex" align="middle" className="m-b-15">
<Grid.Col span={16}>
<Input
data-test={`Table.Column.${column.name}.Title`}
defaultValue={column.title}
onChange={event => handleChangeDebounced({ title: event.target.value })}
/>
</Grid.Col>
<Grid.Col span={8}>
<Radio.Group
className="table-visualization-editor-column-align-content"
defaultValue={column.alignContent}
onChange={event => handleChange({ alignContent: event.target.value })}
>
<Tooltip title="Align left" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="left" data-test={`Table.Column.${column.name}.AlignLeft`}>
<Icon type="align-left" />
</Radio.Button>
</Tooltip>
<Tooltip title="Align center" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="center" data-test={`Table.Column.${column.name}.AlignCenter`}>
<Icon type="align-center" />
</Radio.Button>
</Tooltip>
<Tooltip title="Align right" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="right" data-test={`Table.Column.${column.name}.AlignRight`}>
<Icon type="align-right" />
</Radio.Button>
</Tooltip>
</Radio.Group>
</Grid.Col>
</Grid.Row>

<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-allow-search`}>
<Checkbox
id={`table-column-editor-${column.name}-allow-search`}
data-test={`Table.Column.${column.name}.UseForSearch`}
defaultChecked={column.allowSearch}
onChange={event => handleChange({ allowSearch: event.target.checked })}
/>
<span>Use for search</span>
</label>
</div>

<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-display-as`}>Display as:</label>
<Select
id={`table-column-editor-${column.name}-display-as`}
data-test={`Table.Column.${column.name}.DisplayAs`}
className="w-100"
defaultValue={column.displayAs}
onChange={displayAs => handleChange({ displayAs })}
>
{map(ColumnTypes, ({ friendlyName }, key) => (
<Select.Option key={key} data-test={`Table.Column.${column.name}.DisplayAs.${key}`}>{friendlyName}</Select.Option>
))}
</Select>
</div>

{AdditionalOptions && <AdditionalOptions column={column} onChange={handleChange} />}
</div>
);
}

ColumnEditor.propTypes = {
column: PropTypes.shape({
name: PropTypes.string.isRequired,
title: PropTypes.string,
visible: PropTypes.bool,
alignContent: PropTypes.oneOf(['left', 'center', 'right']),
displayAs: PropTypes.oneOf(keys(ColumnTypes)),
}).isRequired,
onChange: PropTypes.func,
};

ColumnEditor.defaultProps = {
onChange: () => {},
};
78 changes: 78 additions & 0 deletions client/app/visualizations/table/Editor/ColumnsSettings.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { map } from 'lodash';
import React from 'react';
import Collapse from 'antd/lib/collapse';
import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';
import Typography from 'antd/lib/typography';
import { sortableElement } from 'react-sortable-hoc';
import { SortableContainer, DragHandle } from '@/components/sortable';
import { EditorPropTypes } from '@/visualizations';

import ColumnEditor from './ColumnEditor';

const { Text } = Typography;

const SortableItem = sortableElement(Collapse.Panel);

export default function ColumnsSettings({ options, onOptionsChange }) {
function handleColumnChange(newColumn, event) {
if (event) {
event.stopPropagation();
}
const columns = map(options.columns, c => (c.name === newColumn.name ? newColumn : c));
onOptionsChange({ columns });
}

function handleColumnsReorder({ oldIndex, newIndex }) {
const columns = [...options.columns];
columns.splice(newIndex, 0, ...columns.splice(oldIndex, 1));
onOptionsChange({ columns });
}

return (
<SortableContainer
axis="y"
lockAxis="y"
useDragHandle
helperClass="table-editor-columns-dragged-item"
helperContainer={container => container.firstChild}
onSortEnd={handleColumnsReorder}
containerProps={{
className: 'table-visualization-editor-columns',
}}
>
<Collapse bordered={false} defaultActiveKey={[]} expandIconPosition="right">
{map(options.columns, (column, index) => (
<SortableItem
key={column.name}
index={index}
header={(
<React.Fragment>
<DragHandle />
<span data-test={`Table.Column.${column.name}.Name`}>
{column.name}
{(column.title !== '') && (column.title !== column.name) && (
<Text type="secondary" className="m-l-5"><i>({column.title})</i></Text>
)}
</span>
</React.Fragment>
)}
extra={(
<Tooltip title="Toggle visibility" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Icon
data-test={`Table.Column.${column.name}.Visibility`}
type={column.visible ? 'eye' : 'eye-invisible'}
onClick={event => handleColumnChange({ ...column, visible: !column.visible }, event)}
/>
</Tooltip>
)}
>
<ColumnEditor column={column} onChange={handleColumnChange} />
</SortableItem>
))}
</Collapse>
</SortableContainer>
);
}

ColumnsSettings.propTypes = EditorPropTypes;
67 changes: 67 additions & 0 deletions client/app/visualizations/table/Editor/ColumnsSettings.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import enzyme from 'enzyme';

import getOptions from '../getOptions';
import ColumnsSettings from './ColumnsSettings';

function findByTestID(wrapper, testId) {
return wrapper.find(`[data-test="${testId}"]`);
}

function mount(options, done) {
const data = {
columns: [{ name: 'a', type: 'string' }],
rows: [{ a: 'test' }],
};
options = getOptions(options, data);
return enzyme.mount((
<ColumnsSettings
visualizationName="Test"
data={data}
options={options}
onOptionsChange={(changedOptions) => {
expect(changedOptions).toMatchSnapshot();
done();
}}
/>
));
}

describe('Visualizations -> Table -> Editor -> Columns Settings', () => {
test('Toggles column visibility', (done) => {
const el = mount({}, done);

findByTestID(el, 'Table.Column.a.Visibility').first().simulate('click');
});

test('Changes column title', (done) => {
const el = mount({}, done);
findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings

findByTestID(el, 'Table.Column.a.Title').first().simulate('change', { target: { value: 'test' } });
});

test('Changes column alignment', (done) => {
const el = mount({}, done);
findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings

findByTestID(el, 'Table.Column.a.AlignRight').first().find('input')
.simulate('change', { target: { checked: true } });
});

test('Enables search by column data', (done) => {
const el = mount({}, done);
findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings

findByTestID(el, 'Table.Column.a.UseForSearch').first().find('input')
.simulate('change', { target: { checked: true } });
});

test('Changes column display type', (done) => {
const el = mount({}, done);
findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings

findByTestID(el, 'Table.Column.a.DisplayAs').first().simulate('click');
findByTestID(el, 'Table.Column.a.DisplayAs.number').first().simulate('click');
});
});
27 changes: 27 additions & 0 deletions client/app/visualizations/table/Editor/GridSettings.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { map } from 'lodash';
import React from 'react';
import Select from 'antd/lib/select';
import { EditorPropTypes } from '@/visualizations';

const ALLOWED_ITEM_PER_PAGE = [5, 10, 15, 20, 25, 50, 100, 150, 200, 250];

export default function GridSettings({ options, onOptionsChange }) {
return (
<div className="m-b-15">
<label htmlFor="table-editor-items-per-page">Items per page</label>
<Select
id="table-editor-items-per-page"
data-test="Table.ItemsPerPage"
className="w-100"
defaultValue={options.itemsPerPage}
onChange={itemsPerPage => onOptionsChange({ itemsPerPage })}
>
{map(ALLOWED_ITEM_PER_PAGE, value => (
<Select.Option key={`ipp${value}`} value={value} data-test={`Table.ItemsPerPage.${value}`}>{value}</Select.Option>
))}
</Select>
</div>
);
}

GridSettings.propTypes = EditorPropTypes;
36 changes: 36 additions & 0 deletions client/app/visualizations/table/Editor/GridSettings.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import enzyme from 'enzyme';

import getOptions from '../getOptions';
import GridSettings from './GridSettings';

function findByTestID(wrapper, testId) {
return wrapper.find(`[data-test="${testId}"]`);
}

function mount(options, done) {
const data = { columns: [], rows: [] };
options = getOptions(options, data);
return enzyme.mount((
<GridSettings
visualizationName="Test"
data={data}
options={options}
onOptionsChange={(changedOptions) => {
expect(changedOptions).toMatchSnapshot();
done();
}}
/>
));
}

describe('Visualizations -> Table -> Editor -> Grid Settings', () => {
test('Changes items per page', (done) => {
const el = mount({
itemsPerPage: 25,
}, done);

findByTestID(el, 'Table.ItemsPerPage').first().simulate('click');
findByTestID(el, 'Table.ItemsPerPage.100').first().simulate('click');
});
});
Loading

0 comments on commit 7157244

Please sign in to comment.