From 7157244eec2632fbcbd9c59b5987a3cefbabfcdb Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Thu, 24 Oct 2019 12:46:46 +0300 Subject: [PATCH] Migrate Table visualization to React Part 2: Editor (#4175) * 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 --- .../table/Editor/ColumnEditor.jsx | 104 +++++++++++ .../table/Editor/ColumnsSettings.jsx | 78 ++++++++ .../table/Editor/ColumnsSettings.test.js | 67 +++++++ .../table/Editor/GridSettings.jsx | 27 +++ .../table/Editor/GridSettings.test.js | 36 ++++ .../ColumnsSettings.test.js.snap | 166 +++++++++++++++++ .../__snapshots__/GridSettings.test.js.snap | 7 + .../visualizations/table/Editor/editor.less | 37 ++++ .../app/visualizations/table/Editor/index.jsx | 30 +++ .../__snapshots__/boolean.test.js.snap | 19 ++ .../__snapshots__/datetime.test.js.snap | 7 + .../columns/__snapshots__/image.test.js.snap | 25 +++ .../columns/__snapshots__/link.test.js.snap | 25 +++ .../columns/__snapshots__/number.test.js.snap | 7 + .../columns/__snapshots__/text.test.js.snap | 13 ++ .../visualizations/table/columns/boolean.js | 23 --- .../visualizations/table/columns/boolean.jsx | 77 ++++++++ .../table/columns/boolean.test.js | 45 +++++ .../visualizations/table/columns/datetime.js | 23 --- .../visualizations/table/columns/datetime.jsx | 67 +++++++ .../table/columns/datetime.test.js | 35 ++++ .../app/visualizations/table/columns/image.js | 46 ----- .../visualizations/table/columns/image.jsx | 138 ++++++++++++++ .../table/columns/image.test.js | 65 +++++++ .../app/visualizations/table/columns/index.js | 18 ++ .../table/columns/{json.js => json.jsx} | 3 +- .../app/visualizations/table/columns/link.js | 43 ----- .../app/visualizations/table/columns/link.jsx | 128 +++++++++++++ .../visualizations/table/columns/link.test.js | 65 +++++++ .../visualizations/table/columns/number.js | 23 --- .../visualizations/table/columns/number.jsx | 67 +++++++ .../table/columns/number.test.js | 35 ++++ .../app/visualizations/table/columns/text.js | 25 --- .../app/visualizations/table/columns/text.jsx | 68 +++++++ .../visualizations/table/columns/text.test.js | 47 +++++ client/app/visualizations/table/getOptions.js | 111 +++++++++++ client/app/visualizations/table/index.js | 175 ++---------------- .../visualizations/table/table-editor.html | 173 ----------------- .../visualizations/table/table-editor.less | 43 ----- client/app/visualizations/table/utils.js | 20 +- 40 files changed, 1631 insertions(+), 580 deletions(-) create mode 100644 client/app/visualizations/table/Editor/ColumnEditor.jsx create mode 100644 client/app/visualizations/table/Editor/ColumnsSettings.jsx create mode 100644 client/app/visualizations/table/Editor/ColumnsSettings.test.js create mode 100644 client/app/visualizations/table/Editor/GridSettings.jsx create mode 100644 client/app/visualizations/table/Editor/GridSettings.test.js create mode 100644 client/app/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.js.snap create mode 100644 client/app/visualizations/table/Editor/__snapshots__/GridSettings.test.js.snap create mode 100644 client/app/visualizations/table/Editor/editor.less create mode 100644 client/app/visualizations/table/Editor/index.jsx create mode 100644 client/app/visualizations/table/columns/__snapshots__/boolean.test.js.snap create mode 100644 client/app/visualizations/table/columns/__snapshots__/datetime.test.js.snap create mode 100644 client/app/visualizations/table/columns/__snapshots__/image.test.js.snap create mode 100644 client/app/visualizations/table/columns/__snapshots__/link.test.js.snap create mode 100644 client/app/visualizations/table/columns/__snapshots__/number.test.js.snap create mode 100644 client/app/visualizations/table/columns/__snapshots__/text.test.js.snap delete mode 100644 client/app/visualizations/table/columns/boolean.js create mode 100644 client/app/visualizations/table/columns/boolean.jsx create mode 100644 client/app/visualizations/table/columns/boolean.test.js delete mode 100644 client/app/visualizations/table/columns/datetime.js create mode 100644 client/app/visualizations/table/columns/datetime.jsx create mode 100644 client/app/visualizations/table/columns/datetime.test.js delete mode 100644 client/app/visualizations/table/columns/image.js create mode 100644 client/app/visualizations/table/columns/image.jsx create mode 100644 client/app/visualizations/table/columns/image.test.js create mode 100644 client/app/visualizations/table/columns/index.js rename client/app/visualizations/table/columns/{json.js => json.jsx} (93%) delete mode 100644 client/app/visualizations/table/columns/link.js create mode 100644 client/app/visualizations/table/columns/link.jsx create mode 100644 client/app/visualizations/table/columns/link.test.js delete mode 100644 client/app/visualizations/table/columns/number.js create mode 100644 client/app/visualizations/table/columns/number.jsx create mode 100644 client/app/visualizations/table/columns/number.test.js delete mode 100644 client/app/visualizations/table/columns/text.js create mode 100644 client/app/visualizations/table/columns/text.jsx create mode 100644 client/app/visualizations/table/columns/text.test.js create mode 100644 client/app/visualizations/table/getOptions.js delete mode 100644 client/app/visualizations/table/table-editor.html delete mode 100644 client/app/visualizations/table/table-editor.less diff --git a/client/app/visualizations/table/Editor/ColumnEditor.jsx b/client/app/visualizations/table/Editor/ColumnEditor.jsx new file mode 100644 index 0000000000..a79d1918f8 --- /dev/null +++ b/client/app/visualizations/table/Editor/ColumnEditor.jsx @@ -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 ( +
+ + + handleChangeDebounced({ title: event.target.value })} + /> + + + handleChange({ alignContent: event.target.value })} + > + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + +
+ + {AdditionalOptions && } +
+ ); +} + +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: () => {}, +}; diff --git a/client/app/visualizations/table/Editor/ColumnsSettings.jsx b/client/app/visualizations/table/Editor/ColumnsSettings.jsx new file mode 100644 index 0000000000..0e9e7ff9e1 --- /dev/null +++ b/client/app/visualizations/table/Editor/ColumnsSettings.jsx @@ -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 ( + container.firstChild} + onSortEnd={handleColumnsReorder} + containerProps={{ + className: 'table-visualization-editor-columns', + }} + > + + {map(options.columns, (column, index) => ( + + + + {column.name} + {(column.title !== '') && (column.title !== column.name) && ( + ({column.title}) + )} + + + )} + extra={( + + handleColumnChange({ ...column, visible: !column.visible }, event)} + /> + + )} + > + + + ))} + + + ); +} + +ColumnsSettings.propTypes = EditorPropTypes; diff --git a/client/app/visualizations/table/Editor/ColumnsSettings.test.js b/client/app/visualizations/table/Editor/ColumnsSettings.test.js new file mode 100644 index 0000000000..2115eb32da --- /dev/null +++ b/client/app/visualizations/table/Editor/ColumnsSettings.test.js @@ -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(( + { + 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'); + }); +}); diff --git a/client/app/visualizations/table/Editor/GridSettings.jsx b/client/app/visualizations/table/Editor/GridSettings.jsx new file mode 100644 index 0000000000..30ccd58da1 --- /dev/null +++ b/client/app/visualizations/table/Editor/GridSettings.jsx @@ -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 ( +
+ + +
+ ); +} + +GridSettings.propTypes = EditorPropTypes; diff --git a/client/app/visualizations/table/Editor/GridSettings.test.js b/client/app/visualizations/table/Editor/GridSettings.test.js new file mode 100644 index 0000000000..1eb6209a04 --- /dev/null +++ b/client/app/visualizations/table/Editor/GridSettings.test.js @@ -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(( + { + 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'); + }); +}); diff --git a/client/app/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.js.snap b/client/app/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.js.snap new file mode 100644 index 0000000000..669b2e57e0 --- /dev/null +++ b/client/app/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.js.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Table -> Editor -> Columns Settings Changes column alignment 1`] = ` +Object { + "columns": Array [ + Object { + "alignContent": "right", + "allowHTML": true, + "allowSearch": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "displayAs": "string", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "a", + "numberFormat": undefined, + "order": 100000, + "title": "a", + "type": "string", + "visible": true, + }, + ], +} +`; + +exports[`Visualizations -> Table -> Editor -> Columns Settings Changes column display type 1`] = ` +Object { + "columns": Array [ + Object { + "alignContent": "left", + "allowHTML": true, + "allowSearch": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "displayAs": "number", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "a", + "numberFormat": undefined, + "order": 100000, + "title": "a", + "type": "string", + "visible": true, + }, + ], +} +`; + +exports[`Visualizations -> Table -> Editor -> Columns Settings Changes column title 1`] = ` +Object { + "columns": Array [ + Object { + "alignContent": "left", + "allowHTML": true, + "allowSearch": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "displayAs": "string", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "a", + "numberFormat": undefined, + "order": 100000, + "title": "test", + "type": "string", + "visible": true, + }, + ], +} +`; + +exports[`Visualizations -> Table -> Editor -> Columns Settings Enables search by column data 1`] = ` +Object { + "columns": Array [ + Object { + "alignContent": "left", + "allowHTML": true, + "allowSearch": true, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "displayAs": "string", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "a", + "numberFormat": undefined, + "order": 100000, + "title": "a", + "type": "string", + "visible": true, + }, + ], +} +`; + +exports[`Visualizations -> Table -> Editor -> Columns Settings Toggles column visibility 1`] = ` +Object { + "columns": Array [ + Object { + "alignContent": "left", + "allowHTML": true, + "allowSearch": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "displayAs": "string", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "a", + "numberFormat": undefined, + "order": 100000, + "title": "a", + "type": "string", + "visible": false, + }, + ], +} +`; diff --git a/client/app/visualizations/table/Editor/__snapshots__/GridSettings.test.js.snap b/client/app/visualizations/table/Editor/__snapshots__/GridSettings.test.js.snap new file mode 100644 index 0000000000..ce088a680c --- /dev/null +++ b/client/app/visualizations/table/Editor/__snapshots__/GridSettings.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Table -> Editor -> Grid Settings Changes items per page 1`] = ` +Object { + "itemsPerPage": 100, +} +`; diff --git a/client/app/visualizations/table/Editor/editor.less b/client/app/visualizations/table/Editor/editor.less new file mode 100644 index 0000000000..2290298c64 --- /dev/null +++ b/client/app/visualizations/table/Editor/editor.less @@ -0,0 +1,37 @@ +.table-visualization-editor-columns { + .ant-collapse { + background: transparent; + } + + .ant-collapse-item { + background: #ffffff; + + .drag-handle { + height: 20px; + margin-left: -16px; + padding: 0 16px; + } + } + + .table-editor-columns-dragged-item { + z-index: 1; + } +} + +.table-visualization-editor-column { + padding-left: 6px; + + .table-visualization-editor-column-align-content { + display: flex; + align-items: stretch; + justify-content: stretch; + + .ant-radio-button-wrapper { + flex-grow: 1; + text-align: center; + // fit height + height: 35px; + line-height: 33px; + } + } +} diff --git a/client/app/visualizations/table/Editor/index.jsx b/client/app/visualizations/table/Editor/index.jsx new file mode 100644 index 0000000000..e390cee96c --- /dev/null +++ b/client/app/visualizations/table/Editor/index.jsx @@ -0,0 +1,30 @@ +import { merge } from 'lodash'; +import React from 'react'; +import Tabs from 'antd/lib/tabs'; +import { EditorPropTypes } from '@/visualizations'; + +import ColumnsSettings from './ColumnsSettings'; +import GridSettings from './GridSettings'; + +import './editor.less'; + +export default function index(props) { + const { options, onOptionsChange } = props; + + const optionsChanged = (newOptions) => { + onOptionsChange(merge({}, options, newOptions)); + }; + + return ( + + Columns}> + + + Grid}> + + + + ); +} + +index.propTypes = EditorPropTypes; diff --git a/client/app/visualizations/table/columns/__snapshots__/boolean.test.js.snap b/client/app/visualizations/table/columns/__snapshots__/boolean.test.js.snap new file mode 100644 index 0000000000..69ea30bc85 --- /dev/null +++ b/client/app/visualizations/table/columns/__snapshots__/boolean.test.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Table -> Columns -> Boolean Editor Changes value for FALSE 1`] = ` +Object { + "booleanValues": Array [ + "no", + "true", + ], +} +`; + +exports[`Visualizations -> Table -> Columns -> Boolean Editor Changes value for TRUE 1`] = ` +Object { + "booleanValues": Array [ + "false", + "yes", + ], +} +`; diff --git a/client/app/visualizations/table/columns/__snapshots__/datetime.test.js.snap b/client/app/visualizations/table/columns/__snapshots__/datetime.test.js.snap new file mode 100644 index 0000000000..49a0097972 --- /dev/null +++ b/client/app/visualizations/table/columns/__snapshots__/datetime.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Table -> Columns -> Date/Time Editor Changes format 1`] = ` +Object { + "dateTimeFormat": "YYYY/MM/DD HH:ss", +} +`; diff --git a/client/app/visualizations/table/columns/__snapshots__/image.test.js.snap b/client/app/visualizations/table/columns/__snapshots__/image.test.js.snap new file mode 100644 index 0000000000..8fa4e90d37 --- /dev/null +++ b/client/app/visualizations/table/columns/__snapshots__/image.test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Table -> Columns -> Image Editor Changes URL template 1`] = ` +Object { + "imageUrlTemplate": "http://{{ @ }}.jpeg", +} +`; + +exports[`Visualizations -> Table -> Columns -> Image Editor Changes height 1`] = ` +Object { + "imageHeight": "300", +} +`; + +exports[`Visualizations -> Table -> Columns -> Image Editor Changes title template 1`] = ` +Object { + "imageTitleTemplate": "Image {{ @ }}", +} +`; + +exports[`Visualizations -> Table -> Columns -> Image Editor Changes width 1`] = ` +Object { + "imageWidth": "400", +} +`; diff --git a/client/app/visualizations/table/columns/__snapshots__/link.test.js.snap b/client/app/visualizations/table/columns/__snapshots__/link.test.js.snap new file mode 100644 index 0000000000..9c335b26cd --- /dev/null +++ b/client/app/visualizations/table/columns/__snapshots__/link.test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Table -> Columns -> Link Editor Changes URL template 1`] = ` +Object { + "linkUrlTemplate": "http://{{ @ }}/index.html", +} +`; + +exports[`Visualizations -> Table -> Columns -> Link Editor Changes text template 1`] = ` +Object { + "linkTextTemplate": "Text of {{ @ }}", +} +`; + +exports[`Visualizations -> Table -> Columns -> Link Editor Changes title template 1`] = ` +Object { + "linkTitleTemplate": "Title of {{ @ }}", +} +`; + +exports[`Visualizations -> Table -> Columns -> Link Editor Makes link open in new tab 1`] = ` +Object { + "linkOpenInNewTab": true, +} +`; diff --git a/client/app/visualizations/table/columns/__snapshots__/number.test.js.snap b/client/app/visualizations/table/columns/__snapshots__/number.test.js.snap new file mode 100644 index 0000000000..1553913e9e --- /dev/null +++ b/client/app/visualizations/table/columns/__snapshots__/number.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Table -> Columns -> Number Editor Changes format 1`] = ` +Object { + "numberFormat": "0.00%", +} +`; diff --git a/client/app/visualizations/table/columns/__snapshots__/text.test.js.snap b/client/app/visualizations/table/columns/__snapshots__/text.test.js.snap new file mode 100644 index 0000000000..f306cff81b --- /dev/null +++ b/client/app/visualizations/table/columns/__snapshots__/text.test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Table -> Columns -> Text Editor Enables HTML content 1`] = ` +Object { + "allowHTML": true, +} +`; + +exports[`Visualizations -> Table -> Columns -> Text Editor Enables highlight links option 1`] = ` +Object { + "highlightLinks": true, +} +`; diff --git a/client/app/visualizations/table/columns/boolean.js b/client/app/visualizations/table/columns/boolean.js deleted file mode 100644 index c096dd91ec..0000000000 --- a/client/app/visualizations/table/columns/boolean.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable react/prop-types */ -import { createBooleanFormatter } from '@/lib/value-format'; - -export default function initBooleanColumn(column) { - const format = createBooleanFormatter(column.booleanValues); - - function prepareData(row) { - return { - text: format(row[column.name]), - }; - } - - function BooleanColumn({ row }) { - const { text } = prepareData(row); - return text; - } - - BooleanColumn.prepareData = prepareData; - - return BooleanColumn; -} - -initBooleanColumn.friendlyName = 'Boolean'; diff --git a/client/app/visualizations/table/columns/boolean.jsx b/client/app/visualizations/table/columns/boolean.jsx new file mode 100644 index 0000000000..04eeadb505 --- /dev/null +++ b/client/app/visualizations/table/columns/boolean.jsx @@ -0,0 +1,77 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useDebouncedCallback } from 'use-debounce'; +import Input from 'antd/lib/input'; +import { createBooleanFormatter } from '@/lib/value-format'; + +function Editor({ column, onChange }) { + function handleChange(index, value) { + const booleanValues = [...column.booleanValues]; + booleanValues.splice(index, 1, value); + onChange({ booleanValues }); + } + + const [handleChangeDebounced] = useDebouncedCallback(handleChange, 200); + + return ( + +
+
+ + handleChangeDebounced(0, event.target.value)} + /> +
+
+ +
+
+ + handleChangeDebounced(1, event.target.value)} + /> +
+
+
+ ); +} + +Editor.propTypes = { + column: PropTypes.shape({ + name: PropTypes.string.isRequired, + booleanValues: PropTypes.arrayOf(PropTypes.string), + }).isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default function initBooleanColumn(column) { + const format = createBooleanFormatter(column.booleanValues); + + function prepareData(row) { + return { + text: format(row[column.name]), + }; + } + + function BooleanColumn({ row }) { // eslint-disable-line react/prop-types + const { text } = prepareData(row); + return text; + } + + BooleanColumn.prepareData = prepareData; + + return BooleanColumn; +} + +initBooleanColumn.friendlyName = 'Boolean'; +initBooleanColumn.Editor = Editor; diff --git a/client/app/visualizations/table/columns/boolean.test.js b/client/app/visualizations/table/columns/boolean.test.js new file mode 100644 index 0000000000..47596970e3 --- /dev/null +++ b/client/app/visualizations/table/columns/boolean.test.js @@ -0,0 +1,45 @@ +import React from 'react'; +import enzyme from 'enzyme'; + +import Column from './boolean'; + +function findByTestID(wrapper, testId) { + return wrapper.find(`[data-test="${testId}"]`); +} + +function mount(column, done) { + return enzyme.mount(( + { + expect(changedColumn).toMatchSnapshot(); + done(); + }} + /> + )); +} + +describe('Visualizations -> Table -> Columns -> Boolean', () => { + describe('Editor', () => { + test('Changes value for FALSE', (done) => { + const el = mount({ + name: 'a', + booleanValues: ['false', 'true'], + }, done); + + findByTestID(el, 'Table.ColumnEditor.Boolean.False').first().find('input') + .simulate('change', { target: { value: 'no' } }); + }); + + test('Changes value for TRUE', (done) => { + const el = mount({ + name: 'a', + booleanValues: ['false', 'true'], + }, done); + + findByTestID(el, 'Table.ColumnEditor.Boolean.True').first().find('input') + .simulate('change', { target: { value: 'yes' } }); + }); + }); +}); diff --git a/client/app/visualizations/table/columns/datetime.js b/client/app/visualizations/table/columns/datetime.js deleted file mode 100644 index 6aca69bd57..0000000000 --- a/client/app/visualizations/table/columns/datetime.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable react/prop-types */ -import { createDateTimeFormatter } from '@/lib/value-format'; - -export default function initDateTimeColumn(column) { - const format = createDateTimeFormatter(column.dateTimeFormat); - - function prepareData(row) { - return { - text: format(row[column.name]), - }; - } - - function DateTimeColumn({ row }) { - const { text } = prepareData(row); - return text; - } - - DateTimeColumn.prepareData = prepareData; - - return DateTimeColumn; -} - -initDateTimeColumn.friendlyName = 'Date/Time'; diff --git a/client/app/visualizations/table/columns/datetime.jsx b/client/app/visualizations/table/columns/datetime.jsx new file mode 100644 index 0000000000..b8fe5b6e50 --- /dev/null +++ b/client/app/visualizations/table/columns/datetime.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useDebouncedCallback } from 'use-debounce'; +import Input from 'antd/lib/input'; +import Popover from 'antd/lib/popover'; +import Icon from 'antd/lib/icon'; +import { createDateTimeFormatter } from '@/lib/value-format'; + +function Editor({ column, onChange }) { + const [onChangeDebounced] = useDebouncedCallback(onChange, 200); + + return ( + +
+ + onChangeDebounced({ dateTimeFormat: event.target.value })} + /> +
+
+ ); +} + +Editor.propTypes = { + column: PropTypes.shape({ + name: PropTypes.string.isRequired, + dateTimeFormat: PropTypes.string, + }).isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default function initDateTimeColumn(column) { + const format = createDateTimeFormatter(column.dateTimeFormat); + + function prepareData(row) { + return { + text: format(row[column.name]), + }; + } + + function DateTimeColumn({ row }) { // eslint-disable-line react/prop-types + const { text } = prepareData(row); + return text; + } + + DateTimeColumn.prepareData = prepareData; + + return DateTimeColumn; +} + +initDateTimeColumn.friendlyName = 'Date/Time'; +initDateTimeColumn.Editor = Editor; diff --git a/client/app/visualizations/table/columns/datetime.test.js b/client/app/visualizations/table/columns/datetime.test.js new file mode 100644 index 0000000000..f3e2c903f7 --- /dev/null +++ b/client/app/visualizations/table/columns/datetime.test.js @@ -0,0 +1,35 @@ +import React from 'react'; +import enzyme from 'enzyme'; + +import Column from './datetime'; + +function findByTestID(wrapper, testId) { + return wrapper.find(`[data-test="${testId}"]`); +} + +function mount(column, done) { + return enzyme.mount(( + { + expect(changedColumn).toMatchSnapshot(); + done(); + }} + /> + )); +} + +describe('Visualizations -> Table -> Columns -> Date/Time', () => { + describe('Editor', () => { + test('Changes format', (done) => { + const el = mount({ + name: 'a', + dateTimeFormat: 'YYYY-MM-DD HH:mm:ss', + }, done); + + findByTestID(el, 'Table.ColumnEditor.DateTime.Format').first().find('input') + .simulate('change', { target: { value: 'YYYY/MM/DD HH:ss' } }); + }); + }); +}); diff --git a/client/app/visualizations/table/columns/image.js b/client/app/visualizations/table/columns/image.js deleted file mode 100644 index ef7db34acc..0000000000 --- a/client/app/visualizations/table/columns/image.js +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable react/prop-types */ -import { extend, trim } from 'lodash'; -import React from 'react'; -import { formatSimpleTemplate } from '@/lib/value-format'; - -export default function initImageColumn(column) { - function prepareData(row) { - row = extend({ '@': row[column.name] }, row); - - const src = trim(formatSimpleTemplate(column.imageUrlTemplate, row)); - if (src === '') { - return {}; - } - - const width = parseInt(formatSimpleTemplate(column.imageWidth, row), 10); - const height = parseInt(formatSimpleTemplate(column.imageHeight, row), 10); - const title = trim(formatSimpleTemplate(column.imageTitleTemplate, row)); - - const result = { src }; - - if (Number.isFinite(width) && (width > 0)) { - result.width = width; - } - if (Number.isFinite(height) && (height > 0)) { - result.height = height; - } - if (title !== '') { - result.text = title; // `text` is used for search - result.title = title; - result.alt = title; - } - - return result; - } - - function ImageColumn({ row }) { - const { text, ...props } = prepareData(row); - return ; - } - - ImageColumn.prepareData = prepareData; - - return ImageColumn; -} - -initImageColumn.friendlyName = 'Image'; diff --git a/client/app/visualizations/table/columns/image.jsx b/client/app/visualizations/table/columns/image.jsx new file mode 100644 index 0000000000..127621ca94 --- /dev/null +++ b/client/app/visualizations/table/columns/image.jsx @@ -0,0 +1,138 @@ +import { extend, trim } from 'lodash'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { useDebouncedCallback } from 'use-debounce'; +import Input from 'antd/lib/input'; +import Popover from 'antd/lib/popover'; +import Icon from 'antd/lib/icon'; +import { formatSimpleTemplate } from '@/lib/value-format'; + +function Editor({ column, onChange }) { + const [onChangeDebounced] = useDebouncedCallback(onChange, 200); + + return ( + +
+ + onChangeDebounced({ imageUrlTemplate: event.target.value })} + /> +
+ +
+ +
+ onChangeDebounced({ imageWidth: event.target.value })} + /> + × + onChangeDebounced({ imageHeight: event.target.value })} + /> +
+
+ +
+ + onChangeDebounced({ imageTitleTemplate: event.target.value })} + /> +
+ +
+ +
All columns can be referenced using {'{{ column_name }}'} syntax.
+
Use {'{{ @ }}'} to reference current (this) column.
+
This syntax is applicable to URL, Title and Size options.
+ + )} + placement="topLeft" + arrowPointAtCenter + > + + Format specs + +
+
+
+ ); +} + +Editor.propTypes = { + column: PropTypes.shape({ + name: PropTypes.string.isRequired, + imageUrlTemplate: PropTypes.string, + imageWidth: PropTypes.string, + imageHeight: PropTypes.string, + imageTitleTemplate: PropTypes.string, + }).isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default function initImageColumn(column) { + function prepareData(row) { + row = extend({ '@': row[column.name] }, row); + + const src = trim(formatSimpleTemplate(column.imageUrlTemplate, row)); + if (src === '') { + return {}; + } + + const width = parseInt(formatSimpleTemplate(column.imageWidth, row), 10); + const height = parseInt(formatSimpleTemplate(column.imageHeight, row), 10); + const title = trim(formatSimpleTemplate(column.imageTitleTemplate, row)); + + const result = { src }; + + if (Number.isFinite(width) && (width > 0)) { + result.width = width; + } + if (Number.isFinite(height) && (height > 0)) { + result.height = height; + } + if (title !== '') { + result.text = title; // `text` is used for search + result.title = title; + result.alt = title; + } + + return result; + } + + function ImageColumn({ row }) { // eslint-disable-line react/prop-types + const { text, ...props } = prepareData(row); + return ; + } + + ImageColumn.prepareData = prepareData; + + return ImageColumn; +} + +initImageColumn.friendlyName = 'Image'; +initImageColumn.Editor = Editor; diff --git a/client/app/visualizations/table/columns/image.test.js b/client/app/visualizations/table/columns/image.test.js new file mode 100644 index 0000000000..e2ba0dc41c --- /dev/null +++ b/client/app/visualizations/table/columns/image.test.js @@ -0,0 +1,65 @@ +import React from 'react'; +import enzyme from 'enzyme'; + +import Column from './image'; + +function findByTestID(wrapper, testId) { + return wrapper.find(`[data-test="${testId}"]`); +} + +function mount(column, done) { + return enzyme.mount(( + { + expect(changedColumn).toMatchSnapshot(); + done(); + }} + /> + )); +} + +describe('Visualizations -> Table -> Columns -> Image', () => { + describe('Editor', () => { + test('Changes URL template', (done) => { + const el = mount({ + name: 'a', + imageUrlTemplate: '{{ @ }}', + }, done); + + findByTestID(el, 'Table.ColumnEditor.Image.UrlTemplate').first().find('input') + .simulate('change', { target: { value: 'http://{{ @ }}.jpeg' } }); + }); + + test('Changes width', (done) => { + const el = mount({ + name: 'a', + imageWidth: null, + }, done); + + findByTestID(el, 'Table.ColumnEditor.Image.Width').first().find('input') + .simulate('change', { target: { value: '400' } }); + }); + + test('Changes height', (done) => { + const el = mount({ + name: 'a', + imageHeight: null, + }, done); + + findByTestID(el, 'Table.ColumnEditor.Image.Height').first().find('input') + .simulate('change', { target: { value: '300' } }); + }); + + test('Changes title template', (done) => { + const el = mount({ + name: 'a', + imageUrlTemplate: '{{ @ }}', + }, done); + + findByTestID(el, 'Table.ColumnEditor.Image.TitleTemplate').first().find('input') + .simulate('change', { target: { value: 'Image {{ @ }}' } }); + }); + }); +}); diff --git a/client/app/visualizations/table/columns/index.js b/client/app/visualizations/table/columns/index.js new file mode 100644 index 0000000000..f2475e378a --- /dev/null +++ b/client/app/visualizations/table/columns/index.js @@ -0,0 +1,18 @@ +import initTextColumn from './text'; +import initNumberColumn from './number'; +import initDateTimeColumn from './datetime'; +import initBooleanColumn from './boolean'; +import initLinkColumn from './link'; +import initImageColumn from './image'; +import initJsonColumn from './json'; + +// this map should contain all possible values for `column.displayAs` property +export default { + string: initTextColumn, + number: initNumberColumn, + datetime: initDateTimeColumn, + boolean: initBooleanColumn, + link: initLinkColumn, + image: initImageColumn, + json: initJsonColumn, +}; diff --git a/client/app/visualizations/table/columns/json.js b/client/app/visualizations/table/columns/json.jsx similarity index 93% rename from client/app/visualizations/table/columns/json.js rename to client/app/visualizations/table/columns/json.jsx index c92d589e96..6b5a37427f 100644 --- a/client/app/visualizations/table/columns/json.js +++ b/client/app/visualizations/table/columns/json.jsx @@ -1,4 +1,3 @@ -/* eslint-disable react/prop-types */ import { isString, isUndefined } from 'lodash'; import React from 'react'; import JsonViewInteractive from '@/components/json-view-interactive/JsonViewInteractive'; @@ -17,7 +16,7 @@ export default function initJsonColumn(column) { return { text, value: undefined }; } - function JsonColumn({ row }) { + function JsonColumn({ row }) { // eslint-disable-line react/prop-types const { text, value } = prepareData(row); if (isUndefined(value)) { return
{'' + text}
; diff --git a/client/app/visualizations/table/columns/link.js b/client/app/visualizations/table/columns/link.js deleted file mode 100644 index dffc06342d..0000000000 --- a/client/app/visualizations/table/columns/link.js +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable react/prop-types */ -import { extend, trim } from 'lodash'; -import React from 'react'; -import { formatSimpleTemplate } from '@/lib/value-format'; - -export default function initLinkColumn(column) { - function prepareData(row) { - row = extend({ '@': row[column.name] }, row); - - const href = trim(formatSimpleTemplate(column.linkUrlTemplate, row)); - if (href === '') { - return {}; - } - - const title = trim(formatSimpleTemplate(column.linkTitleTemplate, row)); - const text = trim(formatSimpleTemplate(column.linkTextTemplate, row)); - - const result = { - href, - text: text !== '' ? text : href, - }; - - if (title !== '') { - result.title = title; - } - if (column.linkOpenInNewTab) { - result.target = '_blank'; - } - - return result; - } - - function LinkColumn({ row }) { - const { text, ...props } = prepareData(row); - return {text}; - } - - LinkColumn.prepareData = prepareData; - - return LinkColumn; -} - -initLinkColumn.friendlyName = 'Link'; diff --git a/client/app/visualizations/table/columns/link.jsx b/client/app/visualizations/table/columns/link.jsx new file mode 100644 index 0000000000..22af268134 --- /dev/null +++ b/client/app/visualizations/table/columns/link.jsx @@ -0,0 +1,128 @@ +import { extend, trim } from 'lodash'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { useDebouncedCallback } from 'use-debounce'; +import Input from 'antd/lib/input'; +import Checkbox from 'antd/lib/checkbox'; +import Popover from 'antd/lib/popover'; +import Icon from 'antd/lib/icon'; +import { formatSimpleTemplate } from '@/lib/value-format'; + +function Editor({ column, onChange }) { + const [onChangeDebounced] = useDebouncedCallback(onChange, 200); + + return ( + +
+ + onChangeDebounced({ linkUrlTemplate: event.target.value })} + /> +
+ +
+ + onChangeDebounced({ linkTextTemplate: event.target.value })} + /> +
+ +
+ + onChangeDebounced({ linkTitleTemplate: event.target.value })} + /> +
+ +
+ +
+ +
+ +
All columns can be referenced using {'{{ column_name }}'} syntax.
+
Use {'{{ @ }}'} to reference current (this) column.
+
This syntax is applicable to URL, Text and Title options.
+ + )} + placement="topLeft" + arrowPointAtCenter + > + + Format specs + +
+
+
+ ); +} + +Editor.propTypes = { + column: PropTypes.shape({ + name: PropTypes.string.isRequired, + linkUrlTemplate: PropTypes.string, + linkTextTemplate: PropTypes.string, + linkTitleTemplate: PropTypes.string, + linkOpenInNewTab: PropTypes.bool, + }).isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default function initLinkColumn(column) { + function prepareData(row) { + row = extend({ '@': row[column.name] }, row); + + const href = trim(formatSimpleTemplate(column.linkUrlTemplate, row)); + if (href === '') { + return {}; + } + + const title = trim(formatSimpleTemplate(column.linkTitleTemplate, row)); + const text = trim(formatSimpleTemplate(column.linkTextTemplate, row)); + + const result = { + href, + text: text !== '' ? text : href, + }; + + if (title !== '') { + result.title = title; + } + if (column.linkOpenInNewTab) { + result.target = '_blank'; + } + + return result; + } + + function LinkColumn({ row }) { // eslint-disable-line react/prop-types + const { text, ...props } = prepareData(row); + return {text}; + } + + LinkColumn.prepareData = prepareData; + + return LinkColumn; +} + +initLinkColumn.friendlyName = 'Link'; +initLinkColumn.Editor = Editor; diff --git a/client/app/visualizations/table/columns/link.test.js b/client/app/visualizations/table/columns/link.test.js new file mode 100644 index 0000000000..55b3e9da2a --- /dev/null +++ b/client/app/visualizations/table/columns/link.test.js @@ -0,0 +1,65 @@ +import React from 'react'; +import enzyme from 'enzyme'; + +import Column from './link'; + +function findByTestID(wrapper, testId) { + return wrapper.find(`[data-test="${testId}"]`); +} + +function mount(column, done) { + return enzyme.mount(( + { + expect(changedColumn).toMatchSnapshot(); + done(); + }} + /> + )); +} + +describe('Visualizations -> Table -> Columns -> Link', () => { + describe('Editor', () => { + test('Changes URL template', (done) => { + const el = mount({ + name: 'a', + linkUrlTemplate: '{{ @ }}', + }, done); + + findByTestID(el, 'Table.ColumnEditor.Link.UrlTemplate').first().find('input') + .simulate('change', { target: { value: 'http://{{ @ }}/index.html' } }); + }); + + test('Changes text template', (done) => { + const el = mount({ + name: 'a', + linkTextTemplate: '{{ @ }}', + }, done); + + findByTestID(el, 'Table.ColumnEditor.Link.TextTemplate').first().find('input') + .simulate('change', { target: { value: 'Text of {{ @ }}' } }); + }); + + test('Changes title template', (done) => { + const el = mount({ + name: 'a', + linkTitleTemplate: '{{ @ }}', + }, done); + + findByTestID(el, 'Table.ColumnEditor.Link.TitleTemplate').first().find('input') + .simulate('change', { target: { value: 'Title of {{ @ }}' } }); + }); + + test('Makes link open in new tab ', (done) => { + const el = mount({ + name: 'a', + linkOpenInNewTab: false, + }, done); + + findByTestID(el, 'Table.ColumnEditor.Link.OpenInNewTab').first().find('input') + .simulate('change', { target: { checked: true } }); + }); + }); +}); diff --git a/client/app/visualizations/table/columns/number.js b/client/app/visualizations/table/columns/number.js deleted file mode 100644 index 869d4c80ae..0000000000 --- a/client/app/visualizations/table/columns/number.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable react/prop-types */ -import { createNumberFormatter } from '@/lib/value-format'; - -export default function initNumberColumn(column) { - const format = createNumberFormatter(column.numberFormat); - - function prepareData(row) { - return { - text: format(row[column.name]), - }; - } - - function NumberColumn({ row }) { - const { text } = prepareData(row); - return text; - } - - NumberColumn.prepareData = prepareData; - - return NumberColumn; -} - -initNumberColumn.friendlyName = 'Number'; diff --git a/client/app/visualizations/table/columns/number.jsx b/client/app/visualizations/table/columns/number.jsx new file mode 100644 index 0000000000..4f3f83e947 --- /dev/null +++ b/client/app/visualizations/table/columns/number.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useDebouncedCallback } from 'use-debounce'; +import Input from 'antd/lib/input'; +import Popover from 'antd/lib/popover'; +import Icon from 'antd/lib/icon'; +import { createNumberFormatter } from '@/lib/value-format'; + +function Editor({ column, onChange }) { + const [onChangeDebounced] = useDebouncedCallback(onChange, 200); + + return ( + +
+ + onChangeDebounced({ numberFormat: event.target.value })} + /> +
+
+ ); +} + +Editor.propTypes = { + column: PropTypes.shape({ + name: PropTypes.string.isRequired, + numberFormat: PropTypes.string, + }).isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default function initNumberColumn(column) { + const format = createNumberFormatter(column.numberFormat); + + function prepareData(row) { + return { + text: format(row[column.name]), + }; + } + + function NumberColumn({ row }) { // eslint-disable-line react/prop-types + const { text } = prepareData(row); + return text; + } + + NumberColumn.prepareData = prepareData; + + return NumberColumn; +} + +initNumberColumn.friendlyName = 'Number'; +initNumberColumn.Editor = Editor; diff --git a/client/app/visualizations/table/columns/number.test.js b/client/app/visualizations/table/columns/number.test.js new file mode 100644 index 0000000000..d7c5e4d85f --- /dev/null +++ b/client/app/visualizations/table/columns/number.test.js @@ -0,0 +1,35 @@ +import React from 'react'; +import enzyme from 'enzyme'; + +import Column from './number'; + +function findByTestID(wrapper, testId) { + return wrapper.find(`[data-test="${testId}"]`); +} + +function mount(column, done) { + return enzyme.mount(( + { + expect(changedColumn).toMatchSnapshot(); + done(); + }} + /> + )); +} + +describe('Visualizations -> Table -> Columns -> Number', () => { + describe('Editor', () => { + test('Changes format', (done) => { + const el = mount({ + name: 'a', + numberFormat: '0[.]0000', + }, done); + + findByTestID(el, 'Table.ColumnEditor.Number.Format').first().find('input') + .simulate('change', { target: { value: '0.00%' } }); + }); + }); +}); diff --git a/client/app/visualizations/table/columns/text.js b/client/app/visualizations/table/columns/text.js deleted file mode 100644 index 2ee99f3edc..0000000000 --- a/client/app/visualizations/table/columns/text.js +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from 'react'; -import HtmlContent from '@/components/HtmlContent'; -import { createTextFormatter } from '@/lib/value-format'; - -export default function initTextColumn(column) { - const format = createTextFormatter(column.allowHTML && column.highlightLinks); - - function prepareData(row) { - return { - text: format(row[column.name]), - }; - } - - function TextColumn({ row }) { - const { text } = prepareData(row); - return column.allowHTML ? {text} : text; - } - - TextColumn.prepareData = prepareData; - - return TextColumn; -} - -initTextColumn.friendlyName = 'Text'; diff --git a/client/app/visualizations/table/columns/text.jsx b/client/app/visualizations/table/columns/text.jsx new file mode 100644 index 0000000000..0e47f1ab8c --- /dev/null +++ b/client/app/visualizations/table/columns/text.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Checkbox from 'antd/lib/checkbox'; +import HtmlContent from '@/components/HtmlContent'; +import { createTextFormatter } from '@/lib/value-format'; + +function Editor({ column, onChange }) { + return ( + +
+ +
+ + {column.allowHTML && ( +
+ +
+ )} +
+ ); +} + +Editor.propTypes = { + column: PropTypes.shape({ + name: PropTypes.string.isRequired, + allowHTML: PropTypes.bool, + highlightLinks: PropTypes.bool, + }).isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default function initTextColumn(column) { + const format = createTextFormatter(column.allowHTML && column.highlightLinks); + + function prepareData(row) { + return { + text: format(row[column.name]), + }; + } + + function TextColumn({ row }) { // eslint-disable-line react/prop-types + const { text } = prepareData(row); + return column.allowHTML ? {text} : text; + } + + TextColumn.prepareData = prepareData; + + return TextColumn; +} + +initTextColumn.friendlyName = 'Text'; +initTextColumn.Editor = Editor; diff --git a/client/app/visualizations/table/columns/text.test.js b/client/app/visualizations/table/columns/text.test.js new file mode 100644 index 0000000000..47b690a484 --- /dev/null +++ b/client/app/visualizations/table/columns/text.test.js @@ -0,0 +1,47 @@ +import React from 'react'; +import enzyme from 'enzyme'; + +import Column from './text'; + +function findByTestID(wrapper, testId) { + return wrapper.find(`[data-test="${testId}"]`); +} + +function mount(column, done) { + return enzyme.mount(( + { + expect(changedColumn).toMatchSnapshot(); + done(); + }} + /> + )); +} + +describe('Visualizations -> Table -> Columns -> Text', () => { + describe('Editor', () => { + test('Enables HTML content', (done) => { + const el = mount({ + name: 'a', + allowHTML: false, + highlightLinks: false, + }, done); + + findByTestID(el, 'Table.ColumnEditor.Text.AllowHTML').first().find('input') + .simulate('change', { target: { checked: true } }); + }); + + test('Enables highlight links option', (done) => { + const el = mount({ + name: 'a', + allowHTML: true, + highlightLinks: false, + }, done); + + findByTestID(el, 'Table.ColumnEditor.Text.HighlightLinks').first().find('input') + .simulate('change', { target: { checked: true } }); + }); + }); +}); diff --git a/client/app/visualizations/table/getOptions.js b/client/app/visualizations/table/getOptions.js new file mode 100644 index 0000000000..9a5d2da263 --- /dev/null +++ b/client/app/visualizations/table/getOptions.js @@ -0,0 +1,111 @@ +import _ from 'lodash'; +import { getColumnCleanName } from '@/services/query-result'; +import { clientConfig } from '@/services/auth'; + +const DEFAULT_OPTIONS = { + itemsPerPage: 25, +}; + +function getColumnContentAlignment(type) { + return ['integer', 'float', 'boolean', 'date', 'datetime'].indexOf(type) >= 0 ? 'right' : 'left'; +} + +function getDefaultColumnsOptions(columns) { + const displayAs = { + integer: 'number', + float: 'number', + boolean: 'boolean', + date: 'datetime', + datetime: 'datetime', + }; + + return _.map(columns, (col, index) => ({ + name: col.name, + type: col.type, + displayAs: displayAs[col.type] || 'string', + visible: true, + order: 100000 + index, + title: getColumnCleanName(col.name), + allowSearch: false, + alignContent: getColumnContentAlignment(col.type), + // `string` cell options + allowHTML: true, + highlightLinks: false, + })); +} + +function getDefaultFormatOptions(column) { + const dateTimeFormat = { + date: clientConfig.dateFormat || 'DD/MM/YYYY', + datetime: clientConfig.dateTimeFormat || 'DD/MM/YYYY HH:mm', + }; + const numberFormat = { + integer: clientConfig.integerFormat || '0,0', + float: clientConfig.floatFormat || '0,0.00', + }; + return { + dateTimeFormat: dateTimeFormat[column.type], + numberFormat: numberFormat[column.type], + booleanValues: clientConfig.booleanValues || ['false', 'true'], + // `image` cell options + imageUrlTemplate: '{{ @ }}', + imageTitleTemplate: '{{ @ }}', + imageWidth: '', + imageHeight: '', + // `link` cell options + linkUrlTemplate: '{{ @ }}', + linkTextTemplate: '{{ @ }}', + linkTitleTemplate: '{{ @ }}', + linkOpenInNewTab: true, + }; +} + +function wereColumnsReordered(queryColumns, visualizationColumns) { + queryColumns = _.map(queryColumns, col => col.name); + visualizationColumns = _.map(visualizationColumns, col => col.name); + + // Some columns may be removed - so skip them (but keep original order) + visualizationColumns = _.filter(visualizationColumns, col => _.includes(queryColumns, col)); + // Pick query columns that were previously saved with viz (but keep order too) + queryColumns = _.filter(queryColumns, col => _.includes(visualizationColumns, col)); + + // Both array now have the same size as they both contains only common columns + // (in fact, it was an intersection, that kept order of items on both arrays). + // Now check for equality item-by-item; if common columns are in the same order - + // they were not reordered in editor + for (let i = 0; i < queryColumns.length; i += 1) { + if (visualizationColumns[i] !== queryColumns[i]) { + return true; + } + } + return false; +} + +function getColumnsOptions(columns, visualizationColumns) { + const options = getDefaultColumnsOptions(columns); + + if ((wereColumnsReordered(columns, visualizationColumns))) { + visualizationColumns = _.fromPairs(_.map( + visualizationColumns, + (col, index) => [col.name, _.extend({}, col, { order: index })], + )); + } else { + visualizationColumns = _.fromPairs(_.map( + visualizationColumns, + col => [col.name, _.omit(col, 'order')], + )); + } + + _.each(options, col => _.extend(col, visualizationColumns[col.name])); + + return _.sortBy(options, 'order'); +} + +export default function getOptions(options, { columns }) { + options = { ...DEFAULT_OPTIONS, ...options }; + options.columns = _.map( + getColumnsOptions(columns, options.columns), + col => ({ ...getDefaultFormatOptions(col), ...col }), + ); + return options; +} diff --git a/client/app/visualizations/table/index.js b/client/app/visualizations/table/index.js index 6a8b5dfc99..56d7b0c2db 100644 --- a/client/app/visualizations/table/index.js +++ b/client/app/visualizations/table/index.js @@ -1,166 +1,21 @@ -import _ from 'lodash'; -import { angular2react } from 'angular2react'; -import { getColumnCleanName } from '@/services/query-result'; -import { clientConfig } from '@/services/auth'; import { registerVisualization } from '@/visualizations'; -import editorTemplate from './table-editor.html'; -import './table-editor.less'; +import getOptions from './getOptions'; import Renderer from './Renderer'; -import { ColumnTypes } from './utils'; - -const ALLOWED_ITEM_PER_PAGE = [5, 10, 15, 20, 25, 50, 100, 150, 200, 250]; - -const DEFAULT_OPTIONS = { - itemsPerPage: 25, -}; - -function getColumnContentAlignment(type) { - return ['integer', 'float', 'boolean', 'date', 'datetime'].indexOf(type) >= 0 ? 'right' : 'left'; -} - -function getDefaultColumnsOptions(columns) { - const displayAs = { - integer: 'number', - float: 'number', - boolean: 'boolean', - date: 'datetime', - datetime: 'datetime', - }; - - return _.map(columns, (col, index) => ({ - name: col.name, - type: col.type, - displayAs: displayAs[col.type] || 'string', - visible: true, - order: 100000 + index, - title: getColumnCleanName(col.name), - allowSearch: false, - alignContent: getColumnContentAlignment(col.type), - // `string` cell options - allowHTML: true, - highlightLinks: false, - })); -} - -function getDefaultFormatOptions(column) { - const dateTimeFormat = { - date: clientConfig.dateFormat || 'DD/MM/YYYY', - datetime: clientConfig.dateTimeFormat || 'DD/MM/YYYY HH:mm', - }; - const numberFormat = { - integer: clientConfig.integerFormat || '0,0', - float: clientConfig.floatFormat || '0,0.00', - }; - return { - dateTimeFormat: dateTimeFormat[column.type], - numberFormat: numberFormat[column.type], - booleanValues: clientConfig.booleanValues || ['false', 'true'], - // `image` cell options - imageUrlTemplate: '{{ @ }}', - imageTitleTemplate: '{{ @ }}', - imageWidth: '', - imageHeight: '', - // `link` cell options - linkUrlTemplate: '{{ @ }}', - linkTextTemplate: '{{ @ }}', - linkTitleTemplate: '{{ @ }}', - linkOpenInNewTab: true, - }; -} - -function wereColumnsReordered(queryColumns, visualizationColumns) { - queryColumns = _.map(queryColumns, col => col.name); - visualizationColumns = _.map(visualizationColumns, col => col.name); - - // Some columns may be removed - so skip them (but keep original order) - visualizationColumns = _.filter(visualizationColumns, col => _.includes(queryColumns, col)); - // Pick query columns that were previously saved with viz (but keep order too) - queryColumns = _.filter(queryColumns, col => _.includes(visualizationColumns, col)); - - // Both array now have the same size as they both contains only common columns - // (in fact, it was an intersection, that kept order of items on both arrays). - // Now check for equality item-by-item; if common columns are in the same order - - // they were not reordered in editor - for (let i = 0; i < queryColumns.length; i += 1) { - if (visualizationColumns[i] !== queryColumns[i]) { - return true; - } - } - return false; -} - -function getColumnsOptions(columns, visualizationColumns) { - const options = getDefaultColumnsOptions(columns); - - if ((wereColumnsReordered(columns, visualizationColumns))) { - visualizationColumns = _.fromPairs(_.map( - visualizationColumns, - (col, index) => [col.name, _.extend({}, col, { order: index })], - )); - } else { - visualizationColumns = _.fromPairs(_.map( - visualizationColumns, - col => [col.name, _.omit(col, 'order')], - )); - } - - _.each(options, col => _.extend(col, visualizationColumns[col.name])); - - return _.sortBy(options, 'order'); -} - -const GridEditor = { - bindings: { - data: '<', - options: '<', - onOptionsChange: '<', - }, - template: editorTemplate, - controller($scope) { - this.allowedItemsPerPage = ALLOWED_ITEM_PER_PAGE; - this.displayAsOptions = _.map(ColumnTypes, ({ friendlyName: name }, value) => ({ name, value })); - - this.currentTab = 'columns'; - this.setCurrentTab = (tab) => { - this.currentTab = tab; - }; - - $scope.$watch('$ctrl.options', (options) => { - this.onOptionsChange(options); - }, true); - - this.templateHint = ` - All columns can be referenced using {{ column_name }} syntax. - Use {{ @ }} to reference current (this) column. - This syntax is applicable to URL, Title and Size options. - `; - }, -}; - -export default function init(ngModule) { - ngModule.component('gridEditor', GridEditor); - - ngModule.run(($injector) => { - registerVisualization({ - type: 'TABLE', - name: 'Table', - getOptions: (options, { columns }) => { - options = { ...DEFAULT_OPTIONS, ...options }; - options.columns = _.map( - getColumnsOptions(columns, options.columns), - col => ({ ...getDefaultFormatOptions(col), ...col }), - ); - return options; - }, - Renderer, - Editor: angular2react('gridEditor', GridEditor, $injector), - - autoHeight: true, - defaultRows: 14, - defaultColumns: 3, - minColumns: 2, - }); +import Editor from './Editor'; + +export default function init() { + registerVisualization({ + type: 'TABLE', + name: 'Table', + getOptions, + Renderer, + Editor, + + autoHeight: true, + defaultRows: 14, + defaultColumns: 3, + minColumns: 2, }); } diff --git a/client/app/visualizations/table/table-editor.html b/client/app/visualizations/table/table-editor.html deleted file mode 100644 index 46add78a8b..0000000000 --- a/client/app/visualizations/table/table-editor.html +++ /dev/null @@ -1,173 +0,0 @@ -
- - -
-
- - -
-
- -
-
-
-
- - -
-
-
-
- - - -
-
- -
- -
- -
- - -
- -
-
- -
-
- -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- - -
-
- -
-
- - -
- -
- -
- - × - -
-
- -
- - -
- -
- -
-
- -
-
- - -
- -
- - -
- -
- - -
- -
- -
- -
- -
-
-
-
-
diff --git a/client/app/visualizations/table/table-editor.less b/client/app/visualizations/table/table-editor.less deleted file mode 100644 index 79f4c43ae1..0000000000 --- a/client/app/visualizations/table/table-editor.less +++ /dev/null @@ -1,43 +0,0 @@ -.table-editor-container { - .btn-group.btn-group-justified { - display: flex; - align-items: stretch; - justify-content: stretch; - - .btn { - flex-grow: 1; - } - - .btn-xs { - padding: 4px; - } - } - - .table-editor-query-columns { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: stretch; - overflow: auto; - - > div { - min-width: 200px; - width: 200px; - padding: 0 10px; - border-right: 1px solid #f0f0f0; - cursor: move; - - &:last-child { - border-right: none; - } - } - - .table-editor-column-header { - background: rgba(102, 136, 153, 0.05); - padding: 10px; - margin-left: -10px; - margin-right: -10px; - border-bottom: 1px solid #f0f0f0; - } - } -} diff --git a/client/app/visualizations/table/utils.js b/client/app/visualizations/table/utils.js index 7c335af4e4..9d4a433085 100644 --- a/client/app/visualizations/table/utils.js +++ b/client/app/visualizations/table/utils.js @@ -3,25 +3,7 @@ import React from 'react'; import cx from 'classnames'; import Icon from 'antd/lib/icon'; import Tooltip from 'antd/lib/tooltip'; - -import initTextColumn from './columns/text'; -import initNumberColumn from './columns/number'; -import initDateTimeColumn from './columns/datetime'; -import initBooleanColumn from './columns/boolean'; -import initLinkColumn from './columns/link'; -import initImageColumn from './columns/image'; -import initJsonColumn from './columns/json'; - -// this map should contain all possible values for `column.displayAs` property -export const ColumnTypes = { - string: initTextColumn, - number: initNumberColumn, - datetime: initDateTimeColumn, - boolean: initBooleanColumn, - link: initLinkColumn, - image: initImageColumn, - json: initJsonColumn, -}; +import ColumnTypes from './columns'; function nextOrderByDirection(direction) { switch (direction) {