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

Migrate Table visualization to React Part 2: Editor #4175

Merged
merged 16 commits into from
Oct 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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">
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
<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>
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
);
}

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