diff --git a/CHANGELOG.md b/CHANGELOG.md index ec810b3f616..4cfcf3c93a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Added TypeScript definitions for `EuiBasicTable`, `EuiInMemoryTable`, and related components ([#2428](https://github.com/elastic/eui/pull/2428)) + **Bug fixes** - Fixed UX/focus bug in `EuiDataGrid` when using keyboard shortcuts to paginate ([#2602](https://github.com/elastic/eui/pull/2602)) diff --git a/scripts/babel/proptypes-from-ts-props/index.js b/scripts/babel/proptypes-from-ts-props/index.js index 713988f359d..7aa74afcbb6 100644 --- a/scripts/babel/proptypes-from-ts-props/index.js +++ b/scripts/babel/proptypes-from-ts-props/index.js @@ -421,6 +421,15 @@ function getPropTypesForNode(node, optional, state) { // ^^^ Foo case 'TSTypeAnnotation': propType = getPropTypesForNode(node.typeAnnotation, true, state); + + if ( + types.isLiteral(propType) || + (types.isIdentifier(propType) && + propType.name === 'undefined') + ) { + // can't use a literal straight, wrap it with PropTypes.oneOf([ the_literal ]) + propType = convertLiteralToOneOf(types, propType); + } break; // Foo['bar'] @@ -538,7 +547,11 @@ function getPropTypesForNode(node, optional, state) { ]; let propTypeValue = typeProperty.value; - if (types.isLiteral(propTypeValue)) { + if ( + types.isLiteral(propTypeValue) || + (types.isIdentifier(propTypeValue) && + propTypeValue.name === 'undefined') + ) { // can't use a literal straight, wrap it with PropTypes.oneOf([ the_literal ]) propTypeValue = convertLiteralToOneOf(types, propTypeValue); } @@ -628,7 +641,7 @@ function getPropTypesForNode(node, optional, state) { // which don't translate to prop types. .filter(property => property.key != null) .map(property => { - const propertyPropType = + let propertyPropType = property.type === 'TSMethodSignature' ? getPropTypesForNode( { type: 'TSFunctionType' }, @@ -641,6 +654,17 @@ function getPropTypesForNode(node, optional, state) { state ); + if ( + types.isLiteral(propertyPropType) || + (types.isIdentifier(propertyPropType) && + propertyPropType.name === 'undefined') + ) { + propertyPropType = convertLiteralToOneOf(types, propertyPropType); + if (!property.optional) { + propertyPropType = makePropTypeRequired(types, propertyPropType); + } + } + const objectProperty = types.objectProperty( types.identifier( property.key.name || `"${property.key.value}"` diff --git a/scripts/babel/proptypes-from-ts-props/index.test.js b/scripts/babel/proptypes-from-ts-props/index.test.js index dea012cc783..0a3be09d385 100644 --- a/scripts/babel/proptypes-from-ts-props/index.test.js +++ b/scripts/babel/proptypes-from-ts-props/index.test.js @@ -143,6 +143,35 @@ FooComponent.propTypes = { };`); }); + it('understands undefined & null props', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps { + foo: undefined; + bar: null; + bazz: undefined | null; +} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.oneOf([undefined]).isRequired, + bar: PropTypes.oneOf([null]).isRequired, + bazz: PropTypes.oneOf([undefined, null]).isRequired +};`); + }); + it('understands function props', () => { const result = transform( ` @@ -224,6 +253,33 @@ FooComponent.propTypes = { };`); }); + it('understands literal values as a type', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps { + foo: 'bar'; + bazz?: 5; +} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.oneOf(["bar"]).isRequired, + bazz: PropTypes.oneOf([5]) +};`); + }); + }); describe('function propTypes', () => { @@ -1117,7 +1173,7 @@ const FooComponent = () => { }; FooComponent.propTypes = { - type: PropTypes.oneOfType([PropTypes.oneOf(["foo"]), PropTypes.oneOf(["bar"])]), + type: PropTypes.oneOfType([PropTypes.oneOf(["foo"]).isRequired, PropTypes.oneOf(["bar"]).isRequired]).isRequired, value: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]).isRequired };`); }); @@ -1479,6 +1535,40 @@ FooComponent.propTypes = { };`); }); + it('understands discriminated unions', () => { + const result = transform( + ` +import React from 'react' +interface OptA { type: 'a'; value: string; } +interface OptB { type: 'b'; valid: boolean; } +type Option = OptA | OptB; +interface Props { option: Option } +const Foo: React.SFC = ({ option }: Props) => { + return (
{option.type == 'a' ? option.value : option.valid}
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const Foo = ({ + option +}) => { + return
{option.type == 'a' ? option.value : option.valid}
; +}; + +Foo.propTypes = { + option: PropTypes.oneOfType([PropTypes.shape({ + type: PropTypes.oneOf(["a"]).isRequired, + value: PropTypes.string.isRequired + }).isRequired, PropTypes.shape({ + type: PropTypes.oneOf(["b"]).isRequired, + valid: PropTypes.bool.isRequired + }).isRequired]).isRequired +};`); + }); + it('understands an optional Array of strings and numbers', () => { const result = transform( ` diff --git a/src-docs/src/i18ntokens.json b/src-docs/src/i18ntokens.json index 446a147032f..5147bde3460 100644 --- a/src-docs/src/i18ntokens.json +++ b/src-docs/src/i18ntokens.json @@ -13,7 +13,7 @@ "column": 14 } }, - "filepath": "src/components/basic_table/basic_table.js" + "filepath": "src/components/basic_table/basic_table.tsx" }, { "token": "euiBasicTable.selectAllRows", @@ -29,7 +29,7 @@ "column": 77 } }, - "filepath": "src/components/basic_table/basic_table.js" + "filepath": "src/components/basic_table/basic_table.tsx" }, { "token": "euiBasicTable.selectThisRow", @@ -45,7 +45,7 @@ "column": 79 } }, - "filepath": "src/components/basic_table/basic_table.js" + "filepath": "src/components/basic_table/basic_table.tsx" }, { "token": "euiCollapsedItemActions.allActions", @@ -53,15 +53,15 @@ "highlighting": "string", "loc": { "start": { - "line": 107, + "line": 148, "column": 6 }, "end": { - "line": 107, + "line": 148, "column": 80 } }, - "filepath": "src/components/basic_table/collapsed_item_actions.js" + "filepath": "src/components/basic_table/collapsed_item_actions.tsx" }, { "token": "euiCollapsedItemActions.allActions", @@ -69,15 +69,15 @@ "highlighting": "string", "loc": { "start": { - "line": 124, + "line": 165, "column": 6 }, "end": { - "line": 124, + "line": 165, "column": 80 } }, - "filepath": "src/components/basic_table/collapsed_item_actions.js" + "filepath": "src/components/basic_table/collapsed_item_actions.tsx" }, { "token": "euiBottomBar.screenReaderAnnouncement", @@ -1685,11 +1685,11 @@ "highlighting": "string", "loc": { "start": { - "line": 49, + "line": 60, "column": 8 }, "end": { - "line": 49, + "line": 60, "column": 72 } }, diff --git a/src/components/basic_table/__snapshots__/basic_table.test.js.snap b/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap similarity index 99% rename from src/components/basic_table/__snapshots__/basic_table.test.js.snap rename to src/components/basic_table/__snapshots__/basic_table.test.tsx.snap index df21be00f20..69ed8422ad4 100644 --- a/src/components/basic_table/__snapshots__/basic_table.test.js.snap +++ b/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap @@ -374,7 +374,7 @@ exports[`EuiBasicTable empty renders a node as a custom message 1`] = `

no items, click here diff --git a/src/components/basic_table/__snapshots__/collapsed_item_actions.test.js.snap b/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap similarity index 100% rename from src/components/basic_table/__snapshots__/collapsed_item_actions.test.js.snap rename to src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap diff --git a/src/components/basic_table/__snapshots__/custom_item_action.test.js.snap b/src/components/basic_table/__snapshots__/custom_item_action.test.tsx.snap similarity index 75% rename from src/components/basic_table/__snapshots__/custom_item_action.test.js.snap rename to src/components/basic_table/__snapshots__/custom_item_action.test.tsx.snap index bcf25f1339c..578fe7defdf 100644 --- a/src/components/basic_table/__snapshots__/custom_item_action.test.js.snap +++ b/src/components/basic_table/__snapshots__/custom_item_action.test.tsx.snap @@ -2,11 +2,13 @@ exports[`CustomItemAction render 1`] = `

- + > + test +
`; diff --git a/src/components/basic_table/__snapshots__/default_item_action.test.js.snap b/src/components/basic_table/__snapshots__/default_item_action.test.tsx.snap similarity index 66% rename from src/components/basic_table/__snapshots__/default_item_action.test.js.snap rename to src/components/basic_table/__snapshots__/default_item_action.test.tsx.snap index ba136011d28..465e88cbf31 100644 --- a/src/components/basic_table/__snapshots__/default_item_action.test.js.snap +++ b/src/components/basic_table/__snapshots__/default_item_action.test.tsx.snap @@ -1,5 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`DefaultItemAction render - default button 1`] = ` + + + action1 + + +`; + exports[`DefaultItemAction render - button 1`] = ` , + /> , -] + /> + `; diff --git a/src/components/basic_table/__snapshots__/in_memory_table.test.js.snap b/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap similarity index 98% rename from src/components/basic_table/__snapshots__/in_memory_table.test.js.snap rename to src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap index 9c7d57485f8..bffff2ba2e1 100644 --- a/src/components/basic_table/__snapshots__/in_memory_table.test.js.snap +++ b/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap @@ -14,7 +14,6 @@ exports[`EuiInMemoryTable behavior pagination 1`] = ` ] } data-test-subj="test subject string" - executeQueryOptions={Object {}} items={ Array [ Object { @@ -45,7 +44,6 @@ exports[`EuiInMemoryTable behavior pagination 1`] = ` } } responsive={true} - sorting={false} > @@ -1185,7 +1183,7 @@ exports[`EuiInMemoryTable with pagination, selection and sorting 1`] = ` responsive={true} selection={ Object { - "onSelectionChanged": [Function], + "onSelectionChange": [Function], } } sorting={ @@ -1222,7 +1220,6 @@ exports[`EuiInMemoryTable with pagination, selection, sorting and simple search "description": "edit", "name": "Edit", "onClick": [Function], - "type": "button", }, ], "name": "Actions", @@ -1265,7 +1262,7 @@ exports[`EuiInMemoryTable with pagination, selection, sorting and simple search responsive={true} selection={ Object { - "onSelectionChanged": [Function], + "onSelectionChange": [Function], } } sorting={ @@ -1296,7 +1293,6 @@ exports[`EuiInMemoryTable with pagination, selection, sorting and a single recor "description": "edit", "name": "Edit", "onClick": [Function], - "type": "button", }, ], "name": "Actions", @@ -1339,7 +1335,7 @@ exports[`EuiInMemoryTable with pagination, selection, sorting and a single recor responsive={true} selection={ Object { - "onSelectionChanged": [Function], + "onSelectionChange": [Function], } } sorting={ @@ -1398,7 +1394,7 @@ exports[`EuiInMemoryTable with pagination, selection, sorting and column rendere responsive={true} selection={ Object { - "onSelectionChanged": [Function], + "onSelectionChange": [Function], } } sorting={ @@ -1415,9 +1411,6 @@ exports[`EuiInMemoryTable with pagination, selection, sorting and configured sea = (item: T) => EuiIconType; +type ButtonColor = EuiButtonIconColor | EuiButtonEmptyColor; +type EuiButtonIconColorFunction = (item: T) => ButtonColor; + +interface DefaultItemActionBase { + name: string; + description: string; + onClick?: (item: T) => void; + href?: string; + target?: string; + available?: (item: T) => boolean; + enabled?: (item: T) => boolean; + isPrimary?: boolean; + 'data-test-subj'?: string; +} + +export interface DefaultItemEmptyButtonAction + extends DefaultItemActionBase { + type?: 'button'; + color?: EuiButtonEmptyColor | EuiButtonIconColorFunction; +} + +export interface DefaultItemIconButtonAction + extends DefaultItemActionBase { + type: 'icon'; + icon: EuiIconType | IconFunction; + color?: EuiButtonIconColor | EuiButtonIconColorFunction; +} + +export type DefaultItemAction = ExclusiveUnion< + DefaultItemEmptyButtonAction, + DefaultItemIconButtonAction +>; + +export interface CustomItemAction { + render: (item: T, enabled: boolean) => ReactElement; + available?: (item: T) => boolean; + enabled?: (item: T) => boolean; + isPrimary?: boolean; +} + +export type Action = DefaultItemAction | CustomItemAction; diff --git a/src/components/basic_table/basic_table.behavior.test.js b/src/components/basic_table/basic_table.behavior.test.tsx similarity index 89% rename from src/components/basic_table/basic_table.behavior.test.js rename to src/components/basic_table/basic_table.behavior.test.tsx index f9549dac4a4..ab37c9c9f99 100644 --- a/src/components/basic_table/basic_table.behavior.test.js +++ b/src/components/basic_table/basic_table.behavior.test.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import { findTestSubject } from '../../test'; -import { EuiBasicTable } from './basic_table'; +import { EuiBasicTable, EuiBasicTableProps } from './basic_table'; describe('EuiBasicTable', () => { describe('behavior', () => { describe('selected items', () => { - let props; - let component; + let props: EuiBasicTableProps<{ id: string; name: string }>; + let component: ReactWrapper; beforeEach(() => { props = { @@ -22,7 +22,7 @@ describe('EuiBasicTable', () => { }, ], selection: { - onSelectionChanged: () => {}, + onSelectionChange: () => {}, }, onChange: () => {}, }; diff --git a/src/components/basic_table/basic_table.test.js b/src/components/basic_table/basic_table.test.tsx similarity index 83% rename from src/components/basic_table/basic_table.test.js rename to src/components/basic_table/basic_table.test.tsx index 1d40c3a2bd0..15ecfd059bb 100644 --- a/src/components/basic_table/basic_table.test.js +++ b/src/components/basic_table/basic_table.test.tsx @@ -2,7 +2,15 @@ import React from 'react'; import { shallow } from 'enzyme'; import { requiredProps } from '../../test'; -import { EuiBasicTable, getItemId } from './basic_table'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiBasicTableProps, + getItemId, +} from './basic_table'; + +import { SortDirection } from '../../services'; +import { EuiTableFieldDataColumnType } from './table_types'; describe('getItemId', () => { it('returns undefined if no itemId prop is given', () => { @@ -17,24 +25,45 @@ describe('getItemId', () => { }); it('returns the correct id when a function itemId is given', () => { - expect(getItemId({ id: 5 }, () => 6)).toBe(6); - expect(getItemId({ x: 2, y: 4 }, ({ x, y }) => x * y)).toBe(8); + expect(getItemId({ id: 5 }, () => '6')).toBe('6'); + expect( + getItemId( + { x: 2, y: 4 }, + ({ x, y }: { x: number; y: number }) => `${x * y}` + ) + ).toBe('8'); }); }); +interface BasicItem { + id: string; + name: string; +} + +interface AgeItem extends BasicItem { + age: number; +} + +interface CountItem { + id: string; + count: number; +} + +const basicColumns: Array> = [ + { + field: 'name', + name: 'Name', + description: 'description', + }, +]; + describe('EuiBasicTable', () => { describe('empty', () => { test('is rendered', () => { const props = { ...requiredProps, items: [], - columns: [ - { - field: 'name', - name: 'Name', - description: 'description', - }, - ], + columns: basicColumns, }; const component = shallow(); @@ -42,7 +71,7 @@ describe('EuiBasicTable', () => { }); test('renders a string as a custom message', () => { - const props = { + const props: EuiBasicTableProps = { items: [], columns: [ { @@ -59,7 +88,7 @@ describe('EuiBasicTable', () => { }); test('renders a node as a custom message', () => { - const props = { + const props: EuiBasicTableProps = { items: [], columns: [ { @@ -70,7 +99,7 @@ describe('EuiBasicTable', () => { ], noItemsMessage: (

- no items, click here to make some + no items, click here to make some

), }; @@ -82,7 +111,7 @@ describe('EuiBasicTable', () => { describe('rowProps', () => { test('renders rows with custom props from a callback', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -104,13 +133,13 @@ describe('EuiBasicTable', () => { }; }, }; - const component = shallow(); + const component = shallow( {...props} />); expect(component).toMatchSnapshot(); }); test('renders rows with custom props from an object', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -137,7 +166,7 @@ describe('EuiBasicTable', () => { describe('cellProps', () => { test('renders cells with custom props from a callback', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -152,7 +181,7 @@ describe('EuiBasicTable', () => { ], cellProps: (item, column) => { const { id } = item; - const { field } = column; + const { field } = column as EuiTableFieldDataColumnType; return { 'data-test-subj': `cell-${id}-${field}`, className: 'customRowClass', @@ -166,7 +195,7 @@ describe('EuiBasicTable', () => { }); test('renders rows with custom props from an object', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -192,7 +221,7 @@ describe('EuiBasicTable', () => { }); test('itemIdToExpandedRowMap renders an expanded row', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -217,7 +246,7 @@ describe('EuiBasicTable', () => { }); test('with pagination', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -243,7 +272,7 @@ describe('EuiBasicTable', () => { }); test('with pagination - 2nd page', () => { - const props = { + const props: EuiBasicTableProps = { items: [{ id: '1', name: 'name1' }, { id: '2', name: 'name2' }], columns: [ { @@ -265,7 +294,7 @@ describe('EuiBasicTable', () => { }); test('with pagination and error', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -292,7 +321,7 @@ describe('EuiBasicTable', () => { }); test('with pagination, hiding the per page options', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -319,7 +348,7 @@ describe('EuiBasicTable', () => { }); test('with sorting', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -334,7 +363,7 @@ describe('EuiBasicTable', () => { }, ], sorting: { - sort: { field: 'name', direction: 'asc' }, + sort: { field: 'name', direction: SortDirection.ASC }, }, onChange: () => {}, }; @@ -344,7 +373,7 @@ describe('EuiBasicTable', () => { }); test('with sortable columns and sorting disabled', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -366,7 +395,7 @@ describe('EuiBasicTable', () => { }); test('with pagination and selection', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -386,7 +415,7 @@ describe('EuiBasicTable', () => { totalItemCount: 5, }, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, onChange: () => {}, }; @@ -396,7 +425,7 @@ describe('EuiBasicTable', () => { }); test('with pagination, selection and sorting', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -417,10 +446,10 @@ describe('EuiBasicTable', () => { totalItemCount: 5, }, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, sorting: { - sort: { field: 'name', direction: 'asc' }, + sort: { field: 'name', direction: SortDirection.ASC }, }, onChange: () => {}, }; @@ -431,7 +460,7 @@ describe('EuiBasicTable', () => { describe('footers', () => { test('do not render without a column footer definition', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1', age: 20 }, { id: '2', name: 'name2', age: 21 }, @@ -463,7 +492,7 @@ describe('EuiBasicTable', () => { }); test('render with pagination, selection, sorting, and footer', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1', age: 20 }, { id: '2', name: 'name2', age: 21 }, @@ -494,7 +523,7 @@ describe('EuiBasicTable', () => { {items.reduce((acc, cur) => acc + cur.age, 0)}
total items: - {pagination.totalItemCount} + {pagination!.totalItemCount} ), }, @@ -505,10 +534,10 @@ describe('EuiBasicTable', () => { totalItemCount: 5, }, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, sorting: { - sort: { field: 'name', direction: 'asc' }, + sort: { field: 'name', direction: SortDirection.ASC }, }, onChange: () => {}, }; @@ -519,7 +548,7 @@ describe('EuiBasicTable', () => { }); test('with pagination, selection, sorting and column renderer', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -532,7 +561,7 @@ describe('EuiBasicTable', () => { name: 'Name', description: 'description', sortable: true, - render: name => name.toUpperCase(), + render: (name: string) => name.toUpperCase(), }, ], pagination: { @@ -541,10 +570,10 @@ describe('EuiBasicTable', () => { totalItemCount: 5, }, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, sorting: { - sort: { field: 'name', direction: 'asc' }, + sort: { field: 'name', direction: SortDirection.ASC }, }, onChange: () => {}, }; @@ -554,7 +583,7 @@ describe('EuiBasicTable', () => { }); test('with pagination, selection, sorting and column dataType', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', count: 1 }, { id: '2', count: 2 }, @@ -576,10 +605,10 @@ describe('EuiBasicTable', () => { totalItemCount: 5, }, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, sorting: { - sort: { field: 'count', direction: 'asc' }, + sort: { field: 'count', direction: SortDirection.ASC }, }, onChange: () => {}, }; @@ -590,7 +619,7 @@ describe('EuiBasicTable', () => { // here we want to verify that the column renderer takes precedence over the column data type test('with pagination, selection, sorting, column renderer and column dataType', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', count: 1 }, { id: '2', count: 2 }, @@ -604,7 +633,7 @@ describe('EuiBasicTable', () => { description: 'description of count', sortable: true, dataType: 'number', - render: count => 'x'.repeat(count), + render: (count: number) => 'x'.repeat(count), }, ], pagination: { @@ -613,10 +642,10 @@ describe('EuiBasicTable', () => { totalItemCount: 5, }, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, sorting: { - sort: { field: 'count', direction: 'asc' }, + sort: { field: 'count', direction: SortDirection.ASC }, }, onChange: () => {}, }; @@ -626,7 +655,7 @@ describe('EuiBasicTable', () => { }); test('with pagination, selection, sorting and a single record action', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -658,10 +687,10 @@ describe('EuiBasicTable', () => { totalItemCount: 5, }, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, sorting: { - sort: { field: 'name', direction: 'asc' }, + sort: { field: 'name', direction: SortDirection.ASC }, }, onChange: () => {}, }; @@ -671,7 +700,7 @@ describe('EuiBasicTable', () => { }); test('with pagination, selection, sorting and multiple record actions', () => { - const props = { + const props: EuiBasicTableProps = { items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, @@ -709,10 +738,10 @@ describe('EuiBasicTable', () => { totalItemCount: 5, }, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, sorting: { - sort: { field: 'name', direction: 'asc' }, + sort: { field: 'name', direction: SortDirection.ASC }, }, onChange: () => {}, }; diff --git a/src/components/basic_table/basic_table.js b/src/components/basic_table/basic_table.tsx similarity index 67% rename from src/components/basic_table/basic_table.js rename to src/components/basic_table/basic_table.tsx index 532d0f2566d..ed454935b2a 100644 --- a/src/components/basic_table/basic_table.js +++ b/src/components/basic_table/basic_table.tsx @@ -1,202 +1,110 @@ -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, Fragment, HTMLAttributes, ReactNode } from 'react'; import classNames from 'classnames'; +import moment from 'moment'; import { + Direction, formatAuto, formatBoolean, formatDate, formatNumber, formatText, LEFT_ALIGNMENT, - CENTER_ALIGNMENT, RIGHT_ALIGNMENT, - PropertySortType, SortDirection, } from '../../services'; +import { CommonProps } from '../common'; import { isFunction } from '../../services/predicate'; import { get } from '../../services/objects'; import { EuiFlexGroup, EuiFlexItem } from '../flex'; -import { EuiTable } from '../table/table'; -import { EuiTableHeaderCellCheckbox } from '../table/table_header_cell_checkbox'; +// @ts-ignore import { EuiCheckbox } from '../form/checkbox/checkbox'; -import { EuiTableHeaderCell } from '../table/table_header_cell'; -import { EuiTableHeader } from '../table/table_header'; -import { EuiTableBody } from '../table/table_body'; -import { EuiTableFooterCell } from '../table/table_footer_cell'; -import { EuiTableFooter } from '../table/table_footer'; -import { EuiTableRowCellCheckbox } from '../table/table_row_cell_checkbox'; -import { COLORS as BUTTON_ICON_COLORS } from '../button/button_icon/button_icon'; -import { ICON_TYPES } from '../icon'; + +import { + EuiTable, + EuiTableBody, + EuiTableFooter, + EuiTableFooterCell, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableHeaderCellCheckbox, + EuiTableHeaderMobile, + EuiTableRow, + EuiTableRowCell, + EuiTableRowCellCheckbox, + EuiTableSortMobile, +} from '../table'; + import { CollapsedItemActions } from './collapsed_item_actions'; import { ExpandedItemActions } from './expanded_item_actions'; -import { EuiTableRowCell } from '../table/table_row_cell'; -import { EuiTableRow } from '../table/table_row'; -import { PaginationBar, PaginationType } from './pagination_bar'; -import { EuiIcon } from '../icon/icon'; + +import { Pagination, PaginationBar } from './pagination_bar'; +import { EuiIcon } from '../icon'; import { LoadingTableBody } from './loading_table_body'; -import { EuiTableHeaderMobile } from '../table/mobile/table_header_mobile'; -import { EuiTableSortMobile } from '../table/mobile/table_sort_mobile'; -import { withRequiredProp } from '../../utils/prop_types/with_required_prop'; -import { EuiScreenReaderOnly, EuiKeyboardAccessible } from '../accessibility'; +import { EuiKeyboardAccessible, EuiScreenReaderOnly } from '../accessibility'; import { EuiI18n } from '../i18n'; import { EuiDelayRender } from '../delay_render'; import makeId from '../form/form_row/make_id'; +import { Action } from './action_types'; +import { + EuiTableActionsColumnType, + EuiTableComputedColumnType, + EuiTableDataType, + EuiTableFieldDataColumnType, + EuiTableFooterProps, + ItemId, + EuiTableSelectionType, + EuiTableSortingType, +} from './table_types'; +import { EuiTableSortMobileProps } from '../table/mobile/table_sort_mobile'; + +type DataTypeProfiles = Record< + EuiTableDataType, + { + align: typeof LEFT_ALIGNMENT | typeof RIGHT_ALIGNMENT; + render: (value: any) => string; + } +>; -const dataTypesProfiles = { +const dataTypesProfiles: DataTypeProfiles = { auto: { align: LEFT_ALIGNMENT, - render: value => formatAuto(value), + render: (value: any) => formatAuto(value), }, string: { align: LEFT_ALIGNMENT, - render: value => formatText(value), + render: (value: any) => formatText(value), }, number: { align: RIGHT_ALIGNMENT, - render: value => formatNumber(value), + render: (value: number | null) => formatNumber(value), }, boolean: { align: LEFT_ALIGNMENT, - render: value => formatBoolean(value), + render: (value: boolean) => formatBoolean(value), }, date: { align: LEFT_ALIGNMENT, - render: value => formatDate(value), + render: (value: moment.MomentInput) => formatDate(value), }, }; const DATA_TYPES = Object.keys(dataTypesProfiles); -const DefaultItemActionType = PropTypes.shape({ - type: PropTypes.oneOf(['icon', 'button']), // default is 'button' - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - onClick: PropTypes.func, // (item) => void, - href: PropTypes.string, - target: PropTypes.string, - available: PropTypes.func, // (item) => boolean; - enabled: PropTypes.func, // (item) => boolean; - isPrimary: PropTypes.bool, - icon: PropTypes.oneOfType([ - // required when type is 'icon' - PropTypes.oneOf(ICON_TYPES), - PropTypes.func, // (item) => oneOf(ICON_TYPES) - ]), - color: PropTypes.oneOfType([ - PropTypes.oneOf(BUTTON_ICON_COLORS), - PropTypes.func, // (item) => oneOf(ICON_BUTTON_COLORS) - ]), - 'data-test-subj': PropTypes.string, -}); - -const CustomItemActionType = PropTypes.shape({ - render: PropTypes.func.isRequired, // (item, enabled) => PropTypes.node; - available: PropTypes.func, // (item) => boolean; - enabled: PropTypes.func, // (item) => boolean; - isPrimary: PropTypes.bool, -}); - -const SupportedItemActionType = PropTypes.oneOfType([ - DefaultItemActionType, - CustomItemActionType, -]); - -export const ActionsColumnType = PropTypes.shape({ - actions: PropTypes.arrayOf(SupportedItemActionType).isRequired, - name: PropTypes.node, - description: PropTypes.string, - width: PropTypes.string, -}); - -export const FieldDataColumnTypeShape = { - field: PropTypes.string.isRequired, - name: PropTypes.node.isRequired, - description: PropTypes.string, - dataType: PropTypes.oneOf(DATA_TYPES), - width: PropTypes.string, - sortable: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - align: PropTypes.oneOf([LEFT_ALIGNMENT, CENTER_ALIGNMENT, RIGHT_ALIGNMENT]), - truncateText: PropTypes.bool, - render: PropTypes.func, // ((value, record) => PropTypes.node (also see [services/value_renderer] for basic implementations) - footer: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element, - PropTypes.func, // ({ items, pagination }) => PropTypes.node - ]), -}; -export const FieldDataColumnType = PropTypes.shape(FieldDataColumnTypeShape); - -export const ComputedColumnType = PropTypes.shape({ - render: PropTypes.func.isRequired, // (record) => PropTypes.node - name: PropTypes.node, - description: PropTypes.string, - sortable: PropTypes.func, - width: PropTypes.string, - truncateText: PropTypes.bool, -}); - -export const ColumnType = PropTypes.oneOfType([ - FieldDataColumnType, - ComputedColumnType, - ActionsColumnType, -]); - -export const ItemIdType = PropTypes.oneOfType([ - PropTypes.string, // the name of the item id property - PropTypes.func, // (item) => string -]); - -export const SelectionType = PropTypes.shape({ - onSelectionChange: PropTypes.func, // (selection: item[]) => void;, - selectable: PropTypes.func, // (item) => boolean; - selectableMessage: PropTypes.func, // (selectable, item) => string; -}); - -const SortingType = PropTypes.shape({ - sort: PropertySortType, - allowNeutralSort: PropTypes.bool, -}); - -const BasicTablePropTypes = { - itemId: ItemIdType, - itemIdToExpandedRowMap: withRequiredProp( - PropTypes.object, - 'itemId', - 'row expansion uses the itemId prop to identify each row' - ), - items: PropTypes.array.isRequired, - cellProps: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - className: PropTypes.string, - columns: PropTypes.arrayOf(ColumnType).isRequired, - compressed: PropTypes.bool, - error: PropTypes.string, - hasActions: PropTypes.bool, - isExpandable: PropTypes.bool, - isSelectable: PropTypes.bool, - loading: PropTypes.bool, - noItemsMessage: PropTypes.node, - onChange: PropTypes.func, - pagination: PaginationType, - responsive: PropTypes.bool, - rowProps: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - selection: withRequiredProp( - SelectionType, - 'itemId', - 'row selection uses the itemId prop to identify each row' - ), - sorting: SortingType, -}; +interface ItemIdToExpandedRowMap { + [id: string]: ReactNode; +} -export function getItemId(item, itemId) { +export function getItemId(item: T, itemId?: ItemId) { if (itemId) { if (isFunction(itemId)) { return itemId(item); } + // @ts-ignore never mind about the index signature return item[itemId]; } } -function getRowProps(item, rowProps) { +function getRowProps(item: T, rowProps: RowPropsCallback) { if (rowProps) { if (isFunction(rowProps)) { return rowProps(item); @@ -207,7 +115,11 @@ function getRowProps(item, rowProps) { return {}; } -function getCellProps(item, column, cellProps) { +function getCellProps( + item: T, + column: EuiBasicTableColumn, + cellProps: CellPropsCallback +) { if (cellProps) { if (isFunction(cellProps)) { return cellProps(item, column); @@ -218,25 +130,110 @@ function getCellProps(item, column, cellProps) { return {}; } -function getColumnFooter(column, { items, pagination }) { - if (column.footer) { - if (isFunction(column.footer)) { - return column.footer({ items, pagination }); +function getColumnFooter( + column: EuiBasicTableColumn, + { items, pagination }: EuiTableFooterProps +) { + const { footer } = column as EuiTableFieldDataColumnType; + if (footer) { + if (isFunction(footer)) { + return footer({ items, pagination }); } - return column.footer; + return footer; } return undefined; } -export class EuiBasicTable extends Component { - static propTypes = BasicTablePropTypes; +export type EuiBasicTableColumn = + | EuiTableFieldDataColumnType + | EuiTableComputedColumnType + | EuiTableActionsColumnType; + +export interface Criteria { + page?: { + index: number; + size: number; + }; + sort?: { + field: keyof T; + direction: Direction; + }; +} + +export interface CriteriaWithPagination extends Criteria { + page: { + index: number; + size: number; + }; +} + +type CellPropsCallback = (item: T, column: EuiBasicTableColumn) => object; +type RowPropsCallback = (item: T) => object; + +interface BasicTableProps { + itemId?: ItemId; + itemIdToExpandedRowMap?: ItemIdToExpandedRowMap; + items: T[]; + cellProps?: object | CellPropsCallback; + columns: Array>; + compressed?: boolean; + error?: string; + hasActions?: boolean; + isExpandable?: boolean; + isSelectable?: boolean; + loading?: boolean; + noItemsMessage?: ReactNode; + onChange?: (criteria: Criteria) => void; + pagination?: undefined; + responsive?: boolean; + rowProps?: object | RowPropsCallback; + selection?: EuiTableSelectionType; + sorting?: EuiTableSortingType; +} + +type BasicTableWithPaginationProps = Omit< + BasicTableProps, + 'pagination' | 'onChange' +> & { + pagination: Pagination; + onChange?: (criteria: CriteriaWithPagination) => void; +}; + +export type EuiBasicTableProps = CommonProps & + Omit, 'onChange'> & + (BasicTableProps | BasicTableWithPaginationProps); + +interface State { + selection: T[]; +} + +interface SortOptions { + isSorted?: boolean; + isSortAscending?: boolean; + onSort?: () => void; + allowNeutralSort?: boolean; +} + +function hasPagination( + x: EuiBasicTableProps +): x is BasicTableWithPaginationProps { + return x.hasOwnProperty('pagination') && !!x.pagination; +} + +export class EuiBasicTable extends Component< + EuiBasicTableProps, + State +> { static defaultProps = { responsive: true, noItemsMessage: 'No items found', }; - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps( + nextProps: EuiBasicTableProps, + prevState: State + ) { if (!nextProps.selection) { // next props doesn't have a selection, reset our state return { selection: [] }; @@ -244,9 +241,10 @@ export class EuiBasicTable extends Component { const { itemId } = nextProps; const selection = prevState.selection.filter( - selectedItem => + (selectedItem: T) => nextProps.items.findIndex( - item => getItemId(item, itemId) === getItemId(selectedItem, itemId) + (item: T) => + getItemId(item, itemId) === getItemId(selectedItem, itemId) ) !== -1 ); @@ -261,16 +259,13 @@ export class EuiBasicTable extends Component { return null; } - constructor(props) { - super(props); - this.state = { - selection: [], - }; - } + state = { + selection: [], + }; - static buildCriteria(props) { - const criteria = {}; - if (props.pagination) { + buildCriteria(props: EuiBasicTableProps): Criteria { + const criteria: Criteria = {}; + if (hasPagination(props)) { criteria.page = { index: props.pagination.pageIndex, size: props.pagination.pageSize, @@ -282,7 +277,7 @@ export class EuiBasicTable extends Component { return criteria; } - changeSelection(selection) { + changeSelection(selection: T[]) { if (!this.props.selection) { return; } @@ -296,45 +291,50 @@ export class EuiBasicTable extends Component { this.changeSelection([]); } - onPageSizeChange(size) { + onPageSizeChange(size: number) { this.clearSelection(); - const currentCriteria = EuiBasicTable.buildCriteria(this.props); - const criteria = { + const currentCriteria = this.buildCriteria(this.props); + const criteria: CriteriaWithPagination = { ...currentCriteria, page: { index: 0, // when page size changes, we take the user back to the first page size, }, }; - this.props.onChange(criteria); + if (this.props.onChange) { + this.props.onChange(criteria); + } } - onPageChange(index) { + onPageChange(index: number) { this.clearSelection(); - const currentCriteria = EuiBasicTable.buildCriteria(this.props); - const criteria = { + const currentCriteria = this.buildCriteria(this.props); + const criteria: CriteriaWithPagination = { ...currentCriteria, page: { - ...currentCriteria.page, + ...currentCriteria.page!, index, }, }; - this.props.onChange(criteria); + if (this.props.onChange) { + this.props.onChange(criteria); + } } - onColumnSortChange(column) { + onColumnSortChange(column: EuiBasicTableColumn) { this.clearSelection(); - const currentCriteria = EuiBasicTable.buildCriteria(this.props); - let direction = SortDirection.ASC; + const currentCriteria = this.buildCriteria(this.props); + let direction: Direction = SortDirection.ASC; if ( currentCriteria && currentCriteria.sort && - (currentCriteria.sort.field === column.field || + (currentCriteria.sort.field === + (column as EuiTableFieldDataColumnType).field || currentCriteria.sort.field === column.name) ) { direction = SortDirection.reverse(currentCriteria.sort.direction); } - const criteria = { + const criteria: Criteria = { ...currentCriteria, // resetting the page if the criteria has one page: !currentCriteria.page @@ -344,11 +344,15 @@ export class EuiBasicTable extends Component { size: currentCriteria.page.size, }, sort: { - field: column.field || column.name, + field: ((column as EuiTableFieldDataColumnType).field || + column.name) as keyof T, direction, }, }; - this.props.onChange(criteria); + if (this.props.onChange) { + // @ts-ignore complex relationship between pagination's existance and criteria, the code logic ensures this is correctly maintained + this.props.onChange(criteria); + } } render() { @@ -415,10 +419,7 @@ export class EuiBasicTable extends Component { const body = this.renderTableBody(); const footer = this.renderTableFooter(); return ( -
{ - this.tableElement = element; - }}> +
{mobileHeader} {caption} @@ -432,14 +433,17 @@ export class EuiBasicTable extends Component { renderTableMobileSort() { const { columns, sorting } = this.props; - const items = []; + const items: EuiTableSortMobileProps['items'] = []; if (!sorting) { return null; } - columns.forEach((column, index) => { - if (!column.sortable || column.hideForMobile) { + columns.forEach((column: EuiBasicTableColumn, index: number) => { + if ( + !(column as EuiTableFieldDataColumnType).sortable || + (column as EuiTableFieldDataColumnType).hideForMobile + ) { return; } @@ -447,7 +451,9 @@ export class EuiBasicTable extends Component { items.push({ name: column.name, - key: `_data_s_${column.field}_${index}`, + key: `_data_s_${ + (column as EuiTableFieldDataColumnType).field + }_${index}`, onSort: this.resolveColumnOnSort(column), isSorted: !!sortDirection, isSortAscending: sortDirection @@ -481,7 +487,7 @@ export class EuiBasicTable extends Component { ); } - renderSelectAll = isMobile => { + renderSelectAll = (isMobile: boolean) => { const { items, selection } = this.props; if (!selection) { @@ -489,7 +495,7 @@ export class EuiBasicTable extends Component { } const selectableItems = items.filter( - item => !selection.selectable || selection.selectable(item) + (item: T) => !selection.selectable || selection.selectable(item) ); const checked = @@ -499,7 +505,7 @@ export class EuiBasicTable extends Component { const disabled = selectableItems.length === 0; - const onChange = event => { + const onChange = (event: React.ChangeEvent) => { if (event.target.checked) { this.changeSelection(selectableItems); } else { @@ -509,7 +515,7 @@ export class EuiBasicTable extends Component { return ( - {selectAllRows => ( + {(selectAllRows: string) => ( - {this.renderSelectAll()} + {this.renderSelectAll(false)} ); } - columns.forEach((column, index) => { + columns.forEach((column: EuiBasicTableColumn, index: number) => { const { - actions, + field, width, name, - field, align, dataType, sortable, mobileOptions, isMobileHeader, hideForMobile, - } = column; + } = column as EuiTableFieldDataColumnType; const columnAlign = align || this.getAlignForDataType(dataType); // actions column - if (actions) { + if ((column as EuiTableActionsColumnType).actions) { headers.push( ).field) { + const sorting: SortOptions = {}; // computed columns are only sortable if their `sortable` is a function if (this.props.sorting && typeof sortable === 'function') { const sortDirection = this.resolveColumnSortDirection(column); @@ -597,7 +602,7 @@ export class EuiBasicTable extends Component { } // field data column - const sorting = {}; + const sorting: SortOptions = {}; if (this.props.sorting && sortable) { const sortDirection = this.resolveColumnSortDirection(column); sorting.isSorted = !!sortDirection; @@ -640,20 +645,22 @@ export class EuiBasicTable extends Component { ); } - columns.forEach(column => { + columns.forEach((column: EuiBasicTableColumn) => { const footer = getColumnFooter(column, { items, pagination }); - if ( - (column.mobileOptions && column.mobileOptions.only) || - column.isMobileHeader - ) { + const { + mobileOptions, + isMobileHeader, + field, + align, + } = column as EuiTableFieldDataColumnType; + + if ((mobileOptions && mobileOptions!.only) || isMobileHeader) { return; // exclude columns that only exist for mobile headers } if (footer) { footers.push( - + {footer} ); @@ -663,7 +670,7 @@ export class EuiBasicTable extends Component { footers.push( + align={align}> {undefined} ); @@ -684,9 +691,9 @@ export class EuiBasicTable extends Component { return this.renderEmptyBody(); } - const rows = items.map((item, index) => { + const rows = items.map((item: T, index: number) => { // if there's pagination the item's index must be adjusted to the where it is in the whole dataset - const tableItemIndex = this.props.pagination + const tableItemIndex = hasPagination(this.props) ? this.props.pagination.pageIndex * this.props.pagination.pageSize + index : index; @@ -698,7 +705,7 @@ export class EuiBasicTable extends Component { return {rows}; } - renderErrorBody(error) { + renderErrorBody(error: string) { const colSpan = this.props.columns.length + (this.props.selection ? 1 : 0); return ( @@ -731,7 +738,7 @@ export class EuiBasicTable extends Component { ); } - renderItemRow(item, rowIndex) { + renderItemRow(item: T, rowIndex: number) { const { columns, selection, @@ -749,7 +756,8 @@ export class EuiBasicTable extends Component { ? false : this.state.selection && !!this.state.selection.find( - selectedItem => getItemId(selectedItem, itemIdCallback) === itemId + (selectedItem: T) => + getItemId(selectedItem, itemIdCallback) === itemId ); let calculatedHasSelection; @@ -759,25 +767,34 @@ export class EuiBasicTable extends Component { } let calculatedHasActions; - columns.forEach((column, columnIndex) => { - if (column.actions) { + columns.forEach((column: EuiBasicTableColumn, columnIndex: number) => { + if ((column as EuiTableActionsColumnType).actions) { cells.push( this.renderItemActionsCell( itemId, item, - column, - columnIndex, - rowIndex + column as EuiTableActionsColumnType, + columnIndex ) ); calculatedHasActions = true; - } else if (column.field) { + } else if ((column as EuiTableFieldDataColumnType).field) { cells.push( - this.renderItemFieldDataCell(itemId, item, column, columnIndex) + this.renderItemFieldDataCell( + itemId, + item, + column as EuiTableFieldDataColumnType, + columnIndex + ) ); } else { cells.push( - this.renderItemComputedCell(itemId, item, column, columnIndex) + this.renderItemComputedCell( + itemId, + item, + column as EuiTableComputedColumnType, + columnIndex + ) ); } }); @@ -785,12 +802,17 @@ export class EuiBasicTable extends Component { // Occupy full width of table, taking checkbox & mobile only columns into account. let expandedRowColSpan = selection ? columns.length + 1 : columns.length; - const mobileOnlyCols = columns.reduce((num, column) => { - if (column.mobileOptions && column.mobileOptions.only) { + const mobileOnlyCols = columns.reduce((num, column) => { + if ( + (column as EuiTableFieldDataColumnType).mobileOptions && + (column as EuiTableFieldDataColumnType).mobileOptions!.only + ) { return num + 1; } - return column.isMobileHeader ? num + 1 : num + 0; // BWC only + return (column as EuiTableFieldDataColumnType).isMobileHeader + ? num + 1 + : num + 0; // BWC only }, 0); expandedRowColSpan = expandedRowColSpan - mobileOnlyCols; @@ -814,7 +836,7 @@ export class EuiBasicTable extends Component { ); const { rowProps: rowPropsCallback } = this.props; - const rowProps = getRowProps(item, rowPropsCallback); + const rowProps = getRowProps(item, rowPropsCallback as RowPropsCallback); const row = ( - {rowProps.onClick ? ( + {(rowProps as any).onClick ? ( {row} ) : ( row @@ -841,21 +863,21 @@ export class EuiBasicTable extends Component { ); } - renderItemSelectionCell(itemId, item, selected) { + renderItemSelectionCell(itemId: ItemId, item: T, selected: boolean) { const { selection } = this.props; const key = `_selection_column_${itemId}`; const checked = selected; - const disabled = selection.selectable && !selection.selectable(item); + const disabled = selection!.selectable && !selection!.selectable(item); const title = - selection.selectableMessage && - selection.selectableMessage(!disabled, item); - const onChange = event => { + selection!.selectableMessage && + selection!.selectableMessage(!disabled, item); + const onChange = (event: React.ChangeEvent) => { if (event.target.checked) { this.changeSelection([...this.state.selection, item]); } else { const { itemId: itemIdCallback } = this.props; this.changeSelection( - this.state.selection.reduce((selection, selectedItem) => { + this.state.selection.reduce((selection: T[], selectedItem: T) => { if (getItemId(selectedItem, itemIdCallback) !== itemId) { selection.push(selectedItem); } @@ -867,7 +889,7 @@ export class EuiBasicTable extends Component { return ( - {selectThisRow => ( + {(selectThisRow: string) => ( + renderItemActionsCell( + itemId: ItemId, + item: T, + column: EuiTableActionsColumnType, + columnIndex: number + ) { + const actionEnabled = (action: Action) => this.state.selection.length === 0 && (!action.enabled || action.enabled(item)); @@ -903,7 +930,7 @@ export class EuiBasicTable extends Component { actualActions.push({ name: 'All actions', - render: item => { + render: (item: T) => { return ( , + item: T, + column: EuiTableFieldDataColumnType, + columnIndex: number + ) { const { field, render, dataType } = column; const key = `_data_column_${field}_${itemId}_${columnIndex}`; const contentRenderer = render || this.getRendererForDataType(dataType); - const value = get(item, field); + const value = get(item, field as string); const content = contentRenderer(value, item); return this.renderItemCell(item, column, key, content); } - renderItemComputedCell(itemId, item, column, columnIndex) { - const { render, dataType } = column; + renderItemComputedCell( + itemId: ItemId, + item: T, + column: EuiTableComputedColumnType, + columnIndex: number + ) { + const { render } = column; const key = `_computed_column_${itemId}_${columnIndex}`; - const contentRenderer = render || this.getRendererForDataType(dataType); + const contentRenderer = render || this.getRendererForDataType(); const content = contentRenderer(item); return this.renderItemCell(item, column, key, content); } - renderItemCell(item, column, key, content) { + renderItemCell( + item: T, + column: EuiBasicTableColumn, + key: string | number, + content: ReactNode + ) { const { align, render, @@ -967,16 +1009,20 @@ export class EuiBasicTable extends Component { isExpander, textOnly, name, - field, // eslint-disable-line no-unused-vars - description, // eslint-disable-line no-unused-vars - sortable, // eslint-disable-line no-unused-vars - footer, // eslint-disable-line no-unused-vars + field, + description, + sortable, + footer, mobileOptions, ...rest - } = column; + } = column as EuiTableFieldDataColumnType; const columnAlign = align || this.getAlignForDataType(dataType); const { cellProps: cellPropsCallback } = this.props; - const cellProps = getCellProps(item, column, cellPropsCallback); + const cellProps = getCellProps( + item, + column, + cellPropsCallback as CellPropsCallback + ); return ( { + resolveColumnSortDirection = (column: EuiBasicTableColumn) => { const { sorting } = this.props; - if (!sorting || !sorting.sort || !column.sortable) { + const { sortable, field, name } = column as EuiTableFieldDataColumnType; + if (!sorting || !sorting.sort || !sortable) { return; } - if ( - sorting.sort.field === column.field || - sorting.sort.field === column.name - ) { + if (sorting.sort.field === field || sorting.sort.field === name) { return sorting.sort.direction; } }; - resolveColumnOnSort = column => { + resolveColumnOnSort = (column: EuiBasicTableColumn) => { const { sorting } = this.props; - if (!sorting || !column.sortable) { + const { sortable, name } = column as EuiTableFieldDataColumnType; + if (!sorting || !sortable) { return; } if (!this.props.onChange) { - throw new Error(`BasicTable is configured to be sortable on column [${ - column.name - }] but + throw new Error(`BasicTable is configured to be sortable on column [${name}] but [onChange] is not configured. This callback must be implemented to handle the sort requests`); } return () => this.onColumnSortChange(column); }; - getRendererForDataType(dataType = 'auto') { + getRendererForDataType(dataType: EuiTableDataType = 'auto') { const profile = dataTypesProfiles[dataType]; if (!profile) { throw new Error( @@ -1037,7 +1080,7 @@ export class EuiBasicTable extends Component { return profile.render; } - getAlignForDataType(dataType = 'auto') { + getAlignForDataType(dataType: EuiTableDataType = 'auto') { const profile = dataTypesProfiles[dataType]; if (!profile) { throw new Error( diff --git a/src/components/basic_table/collapsed_item_actions.js b/src/components/basic_table/collapsed_item_actions.js deleted file mode 100644 index 1bab6318c0f..00000000000 --- a/src/components/basic_table/collapsed_item_actions.js +++ /dev/null @@ -1,147 +0,0 @@ -import React, { Component } from 'react'; -import { EuiContextMenuItem, EuiContextMenuPanel } from '../context_menu'; -import { EuiPopover } from '../popover'; -import { EuiButtonIcon } from '../button'; -import { EuiToolTip } from '../tool_tip'; -import { EuiI18n } from '../i18n'; - -export class CollapsedItemActions extends Component { - constructor(props) { - super(props); - this.state = { popoverOpen: false }; - } - - togglePopover = () => { - this.setState(prevState => ({ popoverOpen: !prevState.popoverOpen })); - }; - - closePopover = () => { - this.setState({ popoverOpen: false }); - }; - - onPopoverBlur = () => { - // you must be asking... WTF? I know... but this timeout is - // required to make sure we process the onBlur events after the initial - // event cycle. Reference: - // https://medium.com/@jessebeach/dealing-with-focus-and-blur-in-a-composite-widget-in-react-90d3c3b49a9b - window.requestAnimationFrame(() => { - if (!this.popoverDiv.contains(document.activeElement)) { - this.props.onBlur(); - } - }); - }; - - registerPopoverDiv = popoverDiv => { - if (!this.popoverDiv) { - this.popoverDiv = popoverDiv; - this.popoverDiv.addEventListener('focusout', this.onPopoverBlur); - } - }; - - componentWillUnmount() { - if (this.popoverDiv) { - this.popoverDiv.removeEventListener('focusout', this.onPopoverBlur); - } - } - - onClickItem = onClickAction => { - this.closePopover(); - onClickAction(); - }; - - render() { - const { - actions, - itemId, - item, - actionEnabled, - onFocus, - className, - } = this.props; - - const isOpen = this.state.popoverOpen; - - let allDisabled = true; - const controls = actions.reduce((controls, action, index) => { - const key = `action_${itemId}_${index}`; - const available = action.available ? action.available(item) : true; - if (!available) { - return controls; - } - const enabled = actionEnabled(action); - allDisabled = allDisabled && !enabled; - if (action.render) { - const actionControl = action.render(item, enabled); - const actionControlOnClick = - actionControl && actionControl.props && actionControl.props.onClick; - controls.push( - {} - }> - {actionControl} - - ); - } else { - controls.push( - - {action.name} - - ); - } - return controls; - }, []); - - const popoverButton = ( - - {allActions => ( - - )} - - ); - - const withTooltip = !allDisabled && ( - - {allActions => ( - - {popoverButton} - - )} - - ); - - return ( - - - - ); - } -} diff --git a/src/components/basic_table/collapsed_item_actions.test.js b/src/components/basic_table/collapsed_item_actions.test.tsx similarity index 71% rename from src/components/basic_table/collapsed_item_actions.test.js rename to src/components/basic_table/collapsed_item_actions.test.tsx index 0d9d7192ac0..cc925df9265 100644 --- a/src/components/basic_table/collapsed_item_actions.test.js +++ b/src/components/basic_table/collapsed_item_actions.test.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { FocusEvent } from 'react'; import { render } from 'enzyme'; import { CollapsedItemActions } from './collapsed_item_actions'; +import { Action } from './action_types'; describe('CollapsedItemActions', () => { test('render', () => { @@ -14,13 +15,14 @@ describe('CollapsedItemActions', () => { { name: 'custom1', description: 'custom 1', - render: () => {}, + render: () =>
, }, ], itemId: 'id', item: { id: 'xyz' }, - actionEnabled: () => true, - onFocus: () => {}, + actionEnabled: (_: Action<{ id: string }>) => true, + onFocus: (_: FocusEvent) => {}, + onBlur: () => {}, }; const component = render(); diff --git a/src/components/basic_table/collapsed_item_actions.tsx b/src/components/basic_table/collapsed_item_actions.tsx new file mode 100644 index 00000000000..7586465172c --- /dev/null +++ b/src/components/basic_table/collapsed_item_actions.tsx @@ -0,0 +1,188 @@ +import React, { Component, FocusEvent, ReactNode, ReactElement } from 'react'; +import { EuiContextMenuItem, EuiContextMenuPanel } from '../context_menu'; +import { EuiPopover } from '../popover'; +import { EuiButtonIcon } from '../button'; +import { EuiToolTip } from '../tool_tip'; +import { EuiI18n } from '../i18n'; +import { + Action, + CustomItemAction, + DefaultItemIconButtonAction, +} from './action_types'; +import { EuiIconType } from '../icon/icon'; +import { ItemId } from './table_types'; + +export interface CollapsedItemActionsProps { + actions: Array>; + item: T; + itemId: ItemId; + actionEnabled: (action: Action) => boolean; + className?: string; + onFocus?: (event: FocusEvent) => void; + onBlur?: () => void; +} + +interface CollapsedItemActionsState { + popoverOpen: boolean; +} + +function actionIsCustomItemAction( + action: Action +): action is CustomItemAction { + return action.hasOwnProperty('render'); +} + +export class CollapsedItemActions extends Component< + CollapsedItemActionsProps, + CollapsedItemActionsState +> { + private popoverDiv: HTMLDivElement | null = null; + + state = { popoverOpen: false }; + + togglePopover = () => { + this.setState(prevState => ({ popoverOpen: !prevState.popoverOpen })); + }; + + closePopover = () => { + this.setState({ popoverOpen: false }); + }; + + onPopoverBlur = () => { + // you must be asking... WTF? I know... but this timeout is + // required to make sure we process the onBlur events after the initial + // event cycle. Reference: + // https://medium.com/@jessebeach/dealing-with-focus-and-blur-in-a-composite-widget-in-react-90d3c3b49a9b + window.requestAnimationFrame(() => { + if ( + !this.popoverDiv!.contains(document.activeElement) && + this.props.onBlur + ) { + this.props.onBlur(); + } + }); + }; + + registerPopoverDiv = (popoverDiv: HTMLDivElement) => { + if (!this.popoverDiv) { + this.popoverDiv = popoverDiv; + this.popoverDiv.addEventListener('focusout', this.onPopoverBlur); + } + }; + + componentWillUnmount() { + if (this.popoverDiv) { + this.popoverDiv.removeEventListener('focusout', this.onPopoverBlur); + } + } + + onClickItem = (onClickAction: (() => void) | undefined) => { + this.closePopover(); + if (onClickAction) { + onClickAction(); + } + }; + + render() { + const { + actions, + itemId, + item, + actionEnabled, + onFocus, + className, + } = this.props; + + const isOpen = this.state.popoverOpen; + + let allDisabled = true; + const controls = actions.reduce( + (controls, action, index) => { + const key = `action_${itemId}_${index}`; + const available = action.available ? action.available(item) : true; + if (!available) { + return controls; + } + const enabled = actionEnabled(action); + allDisabled = allDisabled && !enabled; + if (actionIsCustomItemAction(action)) { + const customAction = action as CustomItemAction; + const actionControl = customAction.render(item, enabled); + const actionControlOnClick = + actionControl && actionControl.props && actionControl.props.onClick; + controls.push( + {} + }> + {actionControl} + + ); + } else { + const { onClick, name, 'data-test-subj': dataTestSubj } = action; + controls.push( + ).icon as EuiIconType + } + data-test-subj={dataTestSubj} + onClick={this.onClickItem.bind( + null, + onClick ? onClick.bind(null, item) : undefined + )}> + {name} + + ); + } + return controls; + }, + [] + ); + + const popoverButton = ( + + {(allActions: string) => ( + + )} + + ); + + const withTooltip = !allDisabled && ( + + {(allActions: ReactNode) => ( + + {popoverButton} + + )} + + ); + + return ( + + + + ); + } +} diff --git a/src/components/basic_table/custom_item_action.test.js b/src/components/basic_table/custom_item_action.test.tsx similarity index 61% rename from src/components/basic_table/custom_item_action.test.js rename to src/components/basic_table/custom_item_action.test.tsx index ae49c7fee73..0f4ec28c1b5 100644 --- a/src/components/basic_table/custom_item_action.test.js +++ b/src/components/basic_table/custom_item_action.test.tsx @@ -1,17 +1,16 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { CustomItemAction } from './custom_item_action'; +import { CustomItemAction, CustomItemActionProps } from './custom_item_action'; describe('CustomItemAction', () => { test('render', () => { - const props = { + const props: CustomItemActionProps<{ id: string }> = { action: { - name: 'custom1', - description: 'custom 1', - render: () => 'test', + render: () => test, }, enabled: true, item: { id: 'xyz' }, + className: 'test', }; const component = shallow(); diff --git a/src/components/basic_table/custom_item_action.js b/src/components/basic_table/custom_item_action.tsx similarity index 71% rename from src/components/basic_table/custom_item_action.js rename to src/components/basic_table/custom_item_action.tsx index 2b27b2014d0..1560beab7d6 100644 --- a/src/components/basic_table/custom_item_action.js +++ b/src/components/basic_table/custom_item_action.tsx @@ -1,7 +1,25 @@ import React, { Component, cloneElement } from 'react'; +import { CustomItemAction as Action } from './action_types'; -export class CustomItemAction extends Component { - constructor(props) { +export interface CustomItemActionProps { + action: Action; + enabled: boolean; + item: T; + className: string; + index?: number; +} + +interface CustomItemActionState { + hasFocus: boolean; +} + +export class CustomItemAction extends Component< + CustomItemActionProps, + CustomItemActionState +> { + private mounted: boolean; + + constructor(props: CustomItemActionProps) { super(props); this.state = { hasFocus: false }; @@ -46,7 +64,7 @@ export class CustomItemAction extends Component { onFocus: this.onFocus, onBlur: this.onBlur, }); - const style = this.hasFocus() ? { opacity: 1 } : null; + const style = this.hasFocus() ? { opacity: 1 } : undefined; return (
{clonedTool} diff --git a/src/components/basic_table/default_item_action.js b/src/components/basic_table/default_item_action.js deleted file mode 100644 index d34285a9e26..00000000000 --- a/src/components/basic_table/default_item_action.js +++ /dev/null @@ -1,91 +0,0 @@ -import React, { Component } from 'react'; -import { isString } from '../../services/predicate'; -import { EuiButtonEmpty, EuiButtonIcon } from '../button'; -import { EuiToolTip } from '../tool_tip'; - -const defaults = { - color: 'primary', -}; - -export class DefaultItemAction extends Component { - constructor(props) { - super(props); - } - - render() { - const { action, enabled, item, className } = this.props; - - if (!action.onClick && !action.href) { - throw new Error(`Cannot render item action [${ - action.name - }]. Missing required 'onClick' callback - or 'href' string. If you want to provide a custom action control, make sure to define the 'render' callback`); - } - - const onClick = action.onClick ? () => action.onClick(item) : undefined; - const color = this.resolveActionColor(); - const icon = this.resolveActionIcon(); - - let button; - if (action.type === 'icon') { - if (!icon) { - throw new Error(`Cannot render item action [${ - action.name - }]. It is configured to render as an icon but no - icon is provided. Make sure to set the 'icon' property of the action`); - } - button = ( - - ); - } else { - button = ( - - {action.name} - - ); - } - - return enabled && action.description ? ( - - {button} - - ) : ( - button - ); - } - - resolveActionIcon() { - const { action, item } = this.props; - if (action.icon) { - return isString(action.icon) ? action.icon : action.icon(item); - } - } - - resolveActionColor() { - const { action, item } = this.props; - if (action.color) { - return isString(action.color) ? action.color : action.color(item); - } - return defaults.color; - } -} diff --git a/src/components/basic_table/default_item_action.test.js b/src/components/basic_table/default_item_action.test.js deleted file mode 100644 index f87b82390d1..00000000000 --- a/src/components/basic_table/default_item_action.test.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { DefaultItemAction } from './default_item_action'; -import { Random } from '../../services/random'; - -const random = new Random(); - -describe('DefaultItemAction', () => { - test('render - button', () => { - const props = { - action: { - name: 'action1', - description: 'action 1', - type: random.oneOf([undefined, 'button', 'foobar']), - onClick: () => {}, - }, - enabled: true, - item: { id: 'xyz' }, - }; - - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); - - test('render - icon', () => { - const props = { - action: { - name: 'action1', - description: 'action 1', - type: 'icon', - icon: 'trash', - onClick: () => {}, - }, - enabled: true, - item: { id: 'xyz' }, - }; - - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/components/basic_table/default_item_action.test.tsx b/src/components/basic_table/default_item_action.test.tsx new file mode 100644 index 00000000000..813d502e49e --- /dev/null +++ b/src/components/basic_table/default_item_action.test.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { DefaultItemAction } from './default_item_action'; +import { + DefaultItemEmptyButtonAction as EmptyButtonAction, + DefaultItemIconButtonAction as IconButtonAction, +} from './action_types'; + +interface Item { + id: string; +} + +describe('DefaultItemAction', () => { + test('render - default button', () => { + const action: EmptyButtonAction = { + name: 'action1', + description: 'action 1', + onClick: () => {}, + }; + const props = { + action, + enabled: true, + item: { id: 'xyz' }, + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + test('render - button', () => { + const action: EmptyButtonAction = { + name: 'action1', + description: 'action 1', + type: 'button', + onClick: () => {}, + }; + const props = { + action, + enabled: true, + item: { id: 'xyz' }, + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + test('render - icon', () => { + const action: IconButtonAction = { + name: 'action1', + description: 'action 1', + type: 'icon', + icon: 'trash', + onClick: () => {}, + }; + const props = { + action, + enabled: true, + item: { id: 'xyz' }, + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/components/basic_table/default_item_action.tsx b/src/components/basic_table/default_item_action.tsx new file mode 100644 index 00000000000..21718e7d22a --- /dev/null +++ b/src/components/basic_table/default_item_action.tsx @@ -0,0 +1,90 @@ +import React, { ReactElement } from 'react'; +import { isString } from '../../services/predicate'; +import { EuiButtonEmpty, EuiButtonIcon, EuiButtonEmptyColor } from '../button'; +import { EuiToolTip } from '../tool_tip'; +import { + DefaultItemAction as Action, + DefaultItemIconButtonAction as IconButtonAction, +} from './action_types'; + +export interface DefaultItemActionProps { + action: Action; + enabled: boolean; + item: T; + className?: string; +} + +// In order to use generics with an arrow function inside a .tsx file, it's necessary to use +// this `extends` hack and declare the types as shown, instead of declaring the const as a +// FunctionComponent +export const DefaultItemAction = ({ + action, + enabled, + item, + className, +}: DefaultItemActionProps): ReactElement => { + if (!action.onClick && !action.href) { + throw new Error(`Cannot render item action [${ + action.name + }]. Missing required 'onClick' callback + or 'href' string. If you want to provide a custom action control, make sure to define the 'render' callback`); + } + + const onClick = action.onClick ? () => action.onClick!(item) : undefined; + + const resolveActionColor = (action: Action) => + isString(action.color) ? action.color : action.color!(item); + const color = action.color ? resolveActionColor(action) : 'primary'; + + const { icon: buttonIcon } = action as IconButtonAction; + const resolveActionIcon = (action: Action) => + isString(action.icon) ? action.icon : action.icon!(item); + const icon = buttonIcon ? resolveActionIcon(action) : undefined; + + let button; + if (action.type === 'icon') { + if (!icon) { + throw new Error(`Cannot render item action [${ + action.name + }]. It is configured to render as an icon but no + icon is provided. Make sure to set the 'icon' property of the action`); + } + button = ( + + ); + } else { + button = ( + + {action.name} + + ); + } + + return enabled && action.description ? ( + + {button} + + ) : ( + button + ); +}; diff --git a/src/components/basic_table/expanded_item_actions.js b/src/components/basic_table/expanded_item_actions.js deleted file mode 100644 index dfe175cfb54..00000000000 --- a/src/components/basic_table/expanded_item_actions.js +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import { DefaultItemAction } from './default_item_action'; -import { CustomItemAction } from './custom_item_action'; - -export const ExpandedItemActions = ({ - actions, - itemId, - item, - actionEnabled, - className, -}) => { - const moreThanThree = actions.length > 2; - - return actions.reduce((tools, action, index) => { - const available = action.available ? action.available(item) : true; - if (!available) { - return tools; - } - - const enabled = actionEnabled(action); - - const key = `item_action_${itemId}_${index}`; - - const classes = classNames(className, { - expandedItemActions__completelyHide: moreThanThree && index < 2, - }); - - if (action.render) { - // custom action has a render function - tools.push( - - ); - } else { - tools.push( - - ); - } - return tools; - }, []); -}; diff --git a/src/components/basic_table/expanded_item_actions.test.js b/src/components/basic_table/expanded_item_actions.test.tsx similarity index 74% rename from src/components/basic_table/expanded_item_actions.test.js rename to src/components/basic_table/expanded_item_actions.test.tsx index 7fadd55f860..20ef2779af2 100644 --- a/src/components/basic_table/expanded_item_actions.test.js +++ b/src/components/basic_table/expanded_item_actions.test.tsx @@ -1,10 +1,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ExpandedItemActions } from './expanded_item_actions'; +import { + ExpandedItemActions, + ExpandedItemActionsProps, +} from './expanded_item_actions'; describe('ExpandedItemActions', () => { test('render', () => { - const props = { + const props: ExpandedItemActionsProps<{ id: string }> = { actions: [ { name: 'default1', @@ -14,7 +17,7 @@ describe('ExpandedItemActions', () => { { name: 'custom1', description: 'custom 1', - render: () => {}, + render: _item => <>, }, ], itemId: 'xyz', diff --git a/src/components/basic_table/expanded_item_actions.tsx b/src/components/basic_table/expanded_item_actions.tsx new file mode 100644 index 00000000000..d15b1b73698 --- /dev/null +++ b/src/components/basic_table/expanded_item_actions.tsx @@ -0,0 +1,72 @@ +import React, { ReactElement, ReactNode } from 'react'; +import classNames from 'classnames'; +import { DefaultItemAction } from './default_item_action'; +import { CustomItemAction } from './custom_item_action'; +import { + Action, + CustomItemAction as CustomAction, + DefaultItemAction as DefaultAction, +} from './action_types'; +import { ItemId } from './table_types'; + +export interface ExpandedItemActionsProps { + actions: Array>; + itemId: ItemId; + item: T; + actionEnabled: (action: Action) => boolean; + className?: string; +} + +export const ExpandedItemActions = ({ + actions, + itemId, + item, + actionEnabled, + className, +}: ExpandedItemActionsProps): ReactElement => { + const moreThanThree = actions.length > 2; + + return ( + <> + {actions.reduce((tools, action, index) => { + const available = action.available ? action.available(item) : true; + if (!available) { + return tools; + } + + const enabled = actionEnabled(action); + + const key = `item_action_${itemId}_${index}`; + + const classes = classNames(className, { + expandedItemActions__completelyHide: moreThanThree && index < 2, + }); + + if ((action as CustomAction).render) { + // custom action has a render function + tools.push( + } + enabled={enabled} + item={item} + /> + ); + } else { + tools.push( + } + enabled={enabled} + item={item} + /> + ); + } + return tools; + }, [])} + + ); +}; diff --git a/src/components/basic_table/in_memory_table.js b/src/components/basic_table/in_memory_table.js deleted file mode 100644 index 0805d7a44e9..00000000000 --- a/src/components/basic_table/in_memory_table.js +++ /dev/null @@ -1,502 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiBasicTable, - SelectionType, - ItemIdType, - FieldDataColumnTypeShape, - ComputedColumnType, - ActionsColumnType, -} from './basic_table'; -import { defaults as paginationBarDefaults } from './pagination_bar'; -import { isBoolean, isString } from '../../services/predicate'; -import { Comparators, PropertySortType } from '../../services/sort'; -import { - QueryType, - SearchFiltersFiltersType, - SearchBoxConfigPropTypes, - EuiSearchBar, -} from '../search_bar'; -import { EuiSpacer } from '../spacer/spacer'; - -// same as ColumnType from EuiBasicTable, but need to modify the `sortable` type -const ColumnType = PropTypes.oneOfType([ - PropTypes.shape({ - ...FieldDataColumnTypeShape, - sortable: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - }), - ComputedColumnType, - ActionsColumnType, -]); - -const InMemoryTablePropTypes = { - columns: PropTypes.arrayOf(ColumnType).isRequired, - items: PropTypes.array, - loading: PropTypes.bool, - message: PropTypes.node, - error: PropTypes.string, - compressed: PropTypes.bool, - search: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - defaultQuery: QueryType, - box: PropTypes.shape({ - ...SearchBoxConfigPropTypes, - schema: PropTypes.oneOfType([ - // here we enable the user to just assign 'true' to the schema, in which case - // we will auto-generate it out of the columns configuration - PropTypes.bool, - SearchBoxConfigPropTypes.schema, - ]), - }), - filters: SearchFiltersFiltersType, - onChange: PropTypes.func, - executeQueryOptions: PropTypes.shape({ - defaultFields: PropTypes.arrayOf(PropTypes.string), - isClauseMatcher: PropTypes.func, - explain: PropTypes.bool, - }), - }), - ]), - pagination: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - pageSizeOptions: PropTypes.arrayOf(PropTypes.number), - }), - PropTypes.shape({ - initialPageIndex: PropTypes.number, - initialPageSize: PropTypes.number, - pageSizeOptions: PropTypes.arrayOf(PropTypes.number), - }), - ]), - sorting: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - sort: PropertySortType, - }), - ]), - /** - * Set `allowNeutralSort` to false to force column sorting. Defaults to true. - */ - allowNeutralSort: PropTypes.bool, - selection: SelectionType, - itemId: ItemIdType, - rowProps: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - cellProps: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - onTableChange: PropTypes.func, -}; - -const getInitialQuery = search => { - if (!search) { - return; - } - - const query = search.defaultQuery || ''; - return isString(query) ? EuiSearchBar.Query.parse(query) : query; -}; - -const getInitialPagination = pagination => { - if (!pagination) { - return { - pageIndex: undefined, - pageSize: undefined, - }; - } - - const { - initialPageIndex = 0, - initialPageSize, - pageSizeOptions = paginationBarDefaults.pageSizeOptions, - hidePerPageOptions, - } = pagination; - - if ( - !hidePerPageOptions && - initialPageSize && - (!pageSizeOptions || !pageSizeOptions.includes(initialPageSize)) - ) { - throw new Error( - `EuiInMemoryTable received initialPageSize ${initialPageSize}, which wasn't provided within pageSizeOptions.` - ); - } - - const defaultPageSize = pageSizeOptions - ? pageSizeOptions[0] - : paginationBarDefaults.pageSizeOptions[0]; - - return { - pageIndex: initialPageIndex, - pageSize: initialPageSize || defaultPageSize, - pageSizeOptions, - hidePerPageOptions, - }; -}; - -function findColumnByProp(columns, prop, value) { - for (let i = 0; i < columns.length; i++) { - const column = columns[i]; - if (column[prop] === value) { - return column; - } - } -} - -const getInitialSorting = (columns, sorting) => { - if (!sorting || !sorting.sort) { - return { - sortName: undefined, - sortDirection: undefined, - }; - } - - const { field: sortable, direction: sortDirection } = sorting.sort; - - // sortable could be a column's `field` or its `name` - // for backwards compatibility `field` must be checked first - let sortColumn = findColumnByProp(columns, 'field', sortable); - if (sortColumn == null) { - sortColumn = findColumnByProp(columns, 'name', sortable); - } - - if (sortColumn == null) { - return { - sortName: undefined, - sortDirection: undefined, - }; - } - - const sortName = sortColumn.name; - - return { - sortName, - sortDirection, - }; -}; - -export class EuiInMemoryTable extends Component { - static propTypes = InMemoryTablePropTypes; - static defaultProps = { - items: [], - pagination: false, - sorting: false, - responsive: true, - executeQueryOptions: {}, - }; - - static getDerivedStateFromProps(nextProps, prevState) { - if (nextProps.items !== prevState.prevProps.items) { - // We have new items because an external search has completed, so reset pagination state. - return { - prevProps: { - ...prevState.prevProps, - items: nextProps.items, - }, - pageIndex: 0, - }; - } - const { sortName, sortDirection } = getInitialSorting( - nextProps.columns, - nextProps.sorting - ); - if ( - sortName !== prevState.prevProps.sortName || - sortDirection !== prevState.prevProps.sortDirection - ) { - return { - sortName, - sortDirection, - }; - } - return null; - } - - constructor(props) { - super(props); - - const { columns, search, pagination, sorting, allowNeutralSort } = props; - const { - pageIndex, - pageSize, - pageSizeOptions, - hidePerPageOptions, - } = getInitialPagination(pagination); - const { sortName, sortDirection } = getInitialSorting(columns, sorting); - - this.state = { - prevProps: { - items: props.items, - sortName, - sortDirection, - }, - query: getInitialQuery(search), - pageIndex, - pageSize, - pageSizeOptions, - sortName, - sortDirection, - allowNeutralSort: allowNeutralSort === false ? false : true, - hidePerPageOptions, - }; - } - - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - - let { field: sortName, direction: sortDirection } = sort; - - // To keep backwards compatibility reportedSortName needs to be tracked separately - // from sortName; sortName gets stored internally while reportedSortName is sent to the callback - let reportedSortName = sortName; - - // EuiBasicTable returns the column's `field` if it exists instead of `name`, - // map back to `name` if this is the case - for (let i = 0; i < this.props.columns.length; i++) { - const column = this.props.columns[i]; - if (column.field === sortName) { - sortName = column.name; - break; - } - } - - // Allow going back to 'neutral' sorting - if ( - this.state.allowNeutralSort && - this.state.sortName === sortName && - this.state.sortDirection === 'desc' && - sortDirection === 'asc' - ) { - sortName = ''; - reportedSortName = ''; - sortDirection = ''; - } - - if (this.props.onTableChange) { - this.props.onTableChange({ - page, - sort: { - field: reportedSortName, - direction: sortDirection, - }, - }); - } - - this.setState({ - pageIndex, - pageSize, - sortName, - sortDirection, - }); - }; - - onQueryChange = ({ query, queryText, error }) => { - if (this.props.search.onChange) { - const shouldQueryInMemory = this.props.search.onChange({ - query, - queryText, - error, - }); - if (!shouldQueryInMemory) { - return; - } - } - - // Reset pagination state. - this.setState({ - query, - pageIndex: 0, - }); - }; - - renderSearchBar() { - const { search } = this.props; - if (search) { - const { - onChange, // eslint-disable-line no-unused-vars - ...searchBarProps - } = isBoolean(search) ? {} : search; - - if (searchBarProps.box && searchBarProps.box.schema === true) { - searchBarProps.box.schema = this.resolveSearchSchema(); - } - - return ; - } - } - - resolveSearchSchema() { - const { columns } = this.props; - return columns.reduce( - (schema, column) => { - if (column.field) { - const type = column.dataType || 'string'; - schema.fields[column.field] = { type }; - } - return schema; - }, - { strict: true, fields: {} } - ); - } - - getItemSorter() { - const { sortName, sortDirection } = this.state; - - const { columns } = this.props; - - const sortColumn = columns.find(({ name }) => name === sortName); - - if (sortColumn == null) { - // can't return a non-function so return a function that says everything is the same - return () => () => 0; - } - - const sortable = sortColumn.sortable; - - if (typeof sortable === 'function') { - return Comparators.value(sortable, Comparators.default(sortDirection)); - } - - return Comparators.property( - sortColumn.field, - Comparators.default(sortDirection) - ); - } - - getItems() { - const { executeQueryOptions } = this.props; - const { - prevProps: { items }, - } = this.state; - - if (!items.length) { - return { - items: [], - totalItemCount: 0, - }; - } - - const { query, sortName, pageIndex, pageSize } = this.state; - - const matchingItems = query - ? EuiSearchBar.Query.execute(query, items, executeQueryOptions) - : items; - - const sortedItems = sortName - ? matchingItems - .slice(0) // avoid mutating the source array - .sort(this.getItemSorter()) // sort, causes mutation - : matchingItems; - - const visibleItems = pageSize - ? (() => { - const startIndex = pageIndex * pageSize; - return sortedItems.slice( - startIndex, - Math.min(startIndex + pageSize, sortedItems.length) - ); - })() - : sortedItems; - - return { - items: visibleItems, - totalItemCount: matchingItems.length, - }; - } - - render() { - const { - columns, - loading, - message, - error, - selection, - isSelectable, - hasActions, - compressed, - pagination: hasPagination, - sorting: hasSorting, - itemIdToExpandedRowMap, - itemId, - rowProps, - cellProps, - items: _unuseditems, // eslint-disable-line no-unused-vars - search, // eslint-disable-line no-unused-vars - onTableChange, // eslint-disable-line no-unused-vars - executeQueryOptions, // eslint-disable-line no-unused-vars - allowNeutralSort, // eslint-disable-line no-unused-vars - ...rest - } = this.props; - - const { - pageIndex, - pageSize, - pageSizeOptions, - sortName, - sortDirection, - hidePerPageOptions, - } = this.state; - - const { items, totalItemCount } = this.getItems(); - - const pagination = !hasPagination - ? undefined - : { - pageIndex, - pageSize, - pageSizeOptions, - totalItemCount, - hidePerPageOptions, - }; - - // Data loaded from a server can have a default sort order which is meaningful to the - // user, but can't be reproduced with client-side sort logic. So we allow the table to display - // rows in the order in which they're initially loaded by providing an undefined sorting prop. - const sorting = !hasSorting - ? undefined - : { - sort: - !sortName && !sortDirection - ? undefined - : { - field: sortName, - direction: sortDirection, - }, - allowNeutralSort: this.state.allowNeutralSort, - }; - - const searchBar = this.renderSearchBar(); - - const table = ( - - ); - - if (!searchBar) { - return table; - } - - return ( -
- {searchBar} - - {table} -
- ); - } -} diff --git a/src/components/basic_table/in_memory_table.test.js b/src/components/basic_table/in_memory_table.test.tsx similarity index 79% rename from src/components/basic_table/in_memory_table.test.js rename to src/components/basic_table/in_memory_table.test.tsx index 5ea844886e8..798d977024a 100644 --- a/src/components/basic_table/in_memory_table.test.js +++ b/src/components/basic_table/in_memory_table.test.tsx @@ -2,12 +2,34 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { requiredProps } from '../../test'; -import { EuiInMemoryTable } from './in_memory_table'; +import { + EuiInMemoryTable, + EuiInMemoryTableProps, + FilterConfig, +} from './in_memory_table'; import { ENTER } from '../../services/key_codes'; +import { SortDirection } from '../../services'; + +interface BasicItem { + id: number | string; + name: string; +} + +interface StateItem { + active: boolean; + name: string; +} + +interface ComplexItem { + active: boolean; + complex: { + name: string; + }; +} describe('EuiInMemoryTable', () => { test('empty array', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [], columns: [ @@ -24,7 +46,7 @@ describe('EuiInMemoryTable', () => { }); test('with message', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [], columns: [ @@ -42,7 +64,7 @@ describe('EuiInMemoryTable', () => { }); test('with message and loading', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [], columns: [ @@ -61,7 +83,7 @@ describe('EuiInMemoryTable', () => { }); test('with executeQueryOptions', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [], columns: [ @@ -81,7 +103,7 @@ describe('EuiInMemoryTable', () => { }); test('with items', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -102,7 +124,7 @@ describe('EuiInMemoryTable', () => { }); test('with items and expanded item', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -127,7 +149,7 @@ describe('EuiInMemoryTable', () => { }); test('with items and message - expecting to show the items', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, message: 'show me!', items: [ @@ -149,7 +171,7 @@ describe('EuiInMemoryTable', () => { }); test('with pagination', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -173,7 +195,7 @@ describe('EuiInMemoryTable', () => { }); test('with pagination and default page size and index', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -199,7 +221,7 @@ describe('EuiInMemoryTable', () => { }); test('with pagination, default page size and error', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [{ id: '1', name: 'name1' }], error: 'ouch!', @@ -221,7 +243,7 @@ describe('EuiInMemoryTable', () => { }); test('with pagination, hiding the per page options', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -246,7 +268,7 @@ describe('EuiInMemoryTable', () => { describe('sorting', () => { test('with field sorting (off by default)', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '3', name: 'name3' }, @@ -273,7 +295,7 @@ describe('EuiInMemoryTable', () => { }); test('with field sorting (on by default)', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '3', name: 'name3' }, @@ -291,7 +313,7 @@ describe('EuiInMemoryTable', () => { sorting: { sort: { field: 'name', - direction: 'asc', + direction: SortDirection.ASC, }, }, }; @@ -305,7 +327,7 @@ describe('EuiInMemoryTable', () => { }); test('with name sorting', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '3', name: 'name3' }, @@ -323,7 +345,7 @@ describe('EuiInMemoryTable', () => { sorting: { sort: { field: 'Name', - direction: 'desc', + direction: SortDirection.DESC, }, }, }; @@ -337,7 +359,7 @@ describe('EuiInMemoryTable', () => { }); test('verify field sorting precedes name sorting', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name3' }, @@ -361,7 +383,7 @@ describe('EuiInMemoryTable', () => { sorting: { sort: { field: 'name', - direction: 'desc', + direction: SortDirection.DESC, }, }, }; @@ -376,7 +398,7 @@ describe('EuiInMemoryTable', () => { }); test('verify an invalid sort field does not blow everything up', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '3', name: 'name3' }, @@ -394,7 +416,7 @@ describe('EuiInMemoryTable', () => { sorting: { sort: { field: 'something_nonexistant', - direction: 'asc', + direction: SortDirection.ASC, }, }, }; @@ -414,7 +436,7 @@ describe('EuiInMemoryTable', () => { // copy the array to ensure the `items` prop doesn't mutate const itemsProp = items.slice(0); - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: itemsProp, columns: [ @@ -428,7 +450,7 @@ describe('EuiInMemoryTable', () => { sorting: { sort: { field: 'name', - direction: 'desc', + direction: SortDirection.DESC, }, }, }; @@ -439,7 +461,7 @@ describe('EuiInMemoryTable', () => { }); test('with pagination and selection', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -456,7 +478,7 @@ describe('EuiInMemoryTable', () => { ], pagination: true, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, }; const component = shallow(); @@ -465,7 +487,7 @@ describe('EuiInMemoryTable', () => { }); test('with pagination, selection and sorting', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -484,7 +506,7 @@ describe('EuiInMemoryTable', () => { pagination: true, sorting: true, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, }; const component = shallow(); @@ -493,7 +515,7 @@ describe('EuiInMemoryTable', () => { }); test('with pagination, selection, sorting and column renderer', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -507,7 +529,7 @@ describe('EuiInMemoryTable', () => { name: 'Name', description: 'description', sortable: true, - render: name => name.toUpperCase(), + render: (name: any) => name.toUpperCase(), }, ], pagination: { @@ -515,7 +537,7 @@ describe('EuiInMemoryTable', () => { }, sorting: true, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, }; const component = shallow(); @@ -524,7 +546,7 @@ describe('EuiInMemoryTable', () => { }); test('with pagination, selection, sorting and a single record action', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -543,7 +565,6 @@ describe('EuiInMemoryTable', () => { name: 'Actions', actions: [ { - type: 'button', name: 'Edit', description: 'edit', onClick: () => undefined, @@ -554,7 +575,7 @@ describe('EuiInMemoryTable', () => { pagination: true, sorting: true, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, }; const component = shallow(); @@ -563,7 +584,7 @@ describe('EuiInMemoryTable', () => { }); test('with pagination, selection, sorting and simple search', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -582,7 +603,6 @@ describe('EuiInMemoryTable', () => { name: 'Actions', actions: [ { - type: 'button', name: 'Edit', description: 'edit', onClick: () => undefined, @@ -594,7 +614,7 @@ describe('EuiInMemoryTable', () => { sorting: true, search: true, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, }; const component = shallow(); @@ -603,7 +623,7 @@ describe('EuiInMemoryTable', () => { }); test('with pagination, selection, sorting and configured search', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -622,7 +642,6 @@ describe('EuiInMemoryTable', () => { name: 'Actions', actions: [ { - type: 'button', name: 'Edit', description: 'edit', onClick: () => undefined, @@ -636,7 +655,6 @@ describe('EuiInMemoryTable', () => { defaultQuery: 'name:name1', box: { incremental: true, - ...requiredProps, }, filters: [ { @@ -646,10 +664,10 @@ describe('EuiInMemoryTable', () => { name: 'Name1', negatedName: 'Not Name1', }, - ], + ] as FilterConfig[], }, selection: { - onSelectionChanged: () => undefined, + onSelectionChange: () => undefined, }, }; const component = shallow(); @@ -659,35 +677,34 @@ describe('EuiInMemoryTable', () => { describe('search interaction & functionality', () => { it('updates the results as based on the entered query', () => { - const items = [ - { - active: true, - name: 'Kansas', - }, - { - active: true, - name: 'North Dakota', - }, - { - active: false, - name: 'Florida', - }, - ]; - - const columns = [ - { - field: 'active', - name: 'Is Active', - }, - { - field: 'name', - name: 'Name', - }, - ]; - - const search = {}; - - const props = { items, columns, search, className: 'testTable' }; + const props: EuiInMemoryTableProps = { + items: [ + { + active: true, + name: 'Kansas', + }, + { + active: true, + name: 'North Dakota', + }, + { + active: false, + name: 'Florida', + }, + ], + columns: [ + { + field: 'active', + name: 'Is Active', + }, + { + field: 'name', + name: 'Name', + }, + ], + search: {}, + className: 'testTable', + }; const component = mount(); @@ -720,67 +737,86 @@ describe('EuiInMemoryTable', () => { }); it('passes down the executeQueryOptions properly', () => { - const items = [ - { - active: true, - complex: { - name: 'Kansas', + const props: EuiInMemoryTableProps = { + items: [ + { + active: true, + complex: { + name: 'Kansas', + }, }, - }, - { - active: true, - complex: { - name: 'North Dakota', + { + active: true, + complex: { + name: 'North Dakota', + }, }, - }, - { - active: false, - complex: { - name: 'Florida', + { + active: false, + complex: { + name: 'Florida', + }, }, - }, - ]; - - const columns = [ - { - field: 'active', - name: 'Is Active', - }, - { - field: 'complex.name', - name: 'Name', - }, - ]; - - const search = { - defaultQuery: 'No', - executeQueryOptions: { - defaultFields: ['complex.name'], - }, + ], + columns: [ + { + field: 'active', + name: 'Is Active', + }, + { + field: 'complex.name', + name: 'Name', + }, + ], + search: { defaultQuery: 'No' }, + className: 'testTable', + message: No items found!, }; - const message = No items found!; - - const noDefaultFieldsComponent = mount( - - ); + const noDefaultFieldsComponent = mount(); // should render with the no items found text expect(noDefaultFieldsComponent.find('.customMessage').length).toBe(1); // With defaultFields and a search query, we should only see one - const defaultFieldComponent = mount( - - ); + const props2: EuiInMemoryTableProps = { + items: [ + { + active: true, + complex: { + name: 'Kansas', + }, + }, + { + active: true, + complex: { + name: 'North Dakota', + }, + }, + { + active: false, + complex: { + name: 'Florida', + }, + }, + ], + columns: [ + { + field: 'active', + name: 'Is Active', + }, + { + field: 'complex.name', + name: 'Name', + }, + ], + search: { + defaultQuery: 'No', + }, + className: 'testTable', + message: No items found!, + }; + + const defaultFieldComponent = mount(); expect(defaultFieldComponent.find('.testTable EuiTableRow').length).toBe( 1 ); @@ -789,7 +825,7 @@ describe('EuiInMemoryTable', () => { describe('custom column sorting', () => { it('calls the sortable function and uses its return value for sorting', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: 7, name: 'Alfred' }, @@ -801,19 +837,19 @@ describe('EuiInMemoryTable', () => { { field: 'name', name: 'Name', - sortable: ({ id }) => id, + sortable: ({ id }: any) => id, }, ], sorting: { sort: { field: 'name', - direction: 'asc', + direction: SortDirection.ASC, }, }, }; const component = mount(); - expect(component.find('EuiBasicTable').props().items).toEqual([ + expect((component.find('EuiBasicTable').props() as any).items).toEqual([ { id: 3, name: 'Betty' }, { id: 5, name: 'Charlie' }, { id: 7, name: 'Alfred' }, @@ -823,7 +859,7 @@ describe('EuiInMemoryTable', () => { describe('behavior', () => { test('pagination', async () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -851,13 +887,13 @@ describe('EuiInMemoryTable', () => { // forces EuiInMemoryTable's getDerivedStateFromProps to re-execute // this is specifically testing regression against https://github.com/elastic/eui/issues/1007 - component.setProps(); + component.setProps({}); expect(component).toMatchSnapshot(); }); test('onTableChange callback', () => { - const props = { + const props: EuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, @@ -895,7 +931,7 @@ describe('EuiInMemoryTable', () => { }, }); - props.onTableChange.mockClear(); + (props.onTableChange as jest.Mock).mockClear(); component .find( '[data-test-subj*="tableHeaderCell_name_0"] [data-test-subj="tableHeaderSortButton"]' @@ -904,7 +940,7 @@ describe('EuiInMemoryTable', () => { expect(props.onTableChange).toHaveBeenCalledTimes(1); expect(props.onTableChange).toHaveBeenCalledWith({ sort: { - direction: 'asc', + direction: SortDirection.ASC, field: 'name', }, page: { diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx new file mode 100644 index 00000000000..7cbdd917ee4 --- /dev/null +++ b/src/components/basic_table/in_memory_table.tsx @@ -0,0 +1,668 @@ +import React, { Component, ReactNode } from 'react'; +import { + EuiBasicTable, + Criteria, + EuiBasicTableProps, + EuiBasicTableColumn, + CriteriaWithPagination, +} from './basic_table'; +import { + EuiTableFieldDataColumnType, + EuiTableDataType, + EuiTableSortingType, +} from './table_types'; +import { PropertySort } from '../../services'; +import { + defaults as paginationBarDefaults, + Pagination as PaginationBarType, +} from './pagination_bar'; +import { isString } from '../../services/predicate'; +import { Comparators, Direction } from '../../services/sort'; +// @ts-ignore +import { EuiSearchBar } from '../search_bar'; +import { EuiSpacer } from '../spacer'; +import { CommonProps } from '../common'; + +// Search bar types. Should be moved when it is typescriptified. +interface SearchBoxConfig { + placeholder?: string; + incremental?: boolean; + schema?: SchemaType; +} + +interface SchemaType { + strict?: boolean; + fields?: object; + flags?: string[]; +} + +interface IsFilterConfigType { + type: 'is'; + field: string; + name: string; + negatedName?: string; + available?: () => boolean; +} + +interface FieldValueOptionType { + field?: string; + value: any; + name?: string; + view?: ReactNode; +} + +interface FieldValueSelectionFilterConfigType { + type: 'field_value_selection'; + field?: string; + autoClose?: boolean; + name: string; + options: + | FieldValueOptionType[] + | ((query: Query) => Promise); + filterWith?: + | ((name: string, query: string, options: object) => boolean) + | 'prefix' + | 'includes'; + cache?: number; + multiSelect?: boolean | 'and' | 'or'; + loadingMessage?: string; + noOptionsMessage?: string; + searchThreshold?: number; + available?: () => boolean; +} + +interface FieldValueToggleFilterConfigType { + type: 'field_value_toggle'; + field: string; + value: string | number | boolean; + name: string; + negatedName?: string; + available?: () => boolean; + operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; +} + +interface FieldValueToggleGroupFilterItem { + value: string | number | boolean; + name: string; + negatedName?: string; + operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; +} + +interface FieldValueToggleGroupFilterConfigType { + type: 'field_value_toggle_group'; + field: string; + items: FieldValueToggleGroupFilterItem[]; + available?: () => boolean; +} + +export type FilterConfig = + | IsFilterConfigType + | FieldValueSelectionFilterConfigType + | FieldValueToggleFilterConfigType + | FieldValueToggleGroupFilterConfigType; + +type SearchBox = Omit & { + schema?: boolean | SchemaType; +}; + +/* Should point at search_bar/query type when it is converted to typescript */ +type Query = any; + +interface onChangeArgument { + query: Query; + queryText: string; + error: string; +} + +interface EuiSearchBarProps { + /** + The initial query the bar will hold when first mounted + */ + defaultQuery?: Query; + /** + If you wish to use the search bar as a controlled component, continuously pass the query + via this prop + */ + query?: Query; + /** + Configures the search box. Set `placeholder` to change the placeholder text in the box and + `incremental` to support incremental (as you type) search. + */ + box?: SearchBox; + /** + An array of search filters. + */ + filters?: FilterConfig[]; + /** + * Tools which go to the left of the search bar. + */ + toolsLeft?: React.ReactNode; + /** + * Tools which go to the right of the search bar. + */ + toolsRight?: React.ReactNode; + /** + * Date formatter to use when parsing date values + */ + dateFormat?: object; + onChange?: (values: onChangeArgument) => boolean | void; +} + +function isEuiSearchBarProps( + x: EuiInMemoryTableProps['search'] +): x is EuiSearchBarProps { + return typeof x !== 'boolean'; +} + +type Search = boolean | EuiSearchBarProps; + +interface PaginationOptions { + initialPageIndex?: number; + initialPageSize?: number; + pageSizeOptions?: number[]; + hidePerPageOptions?: boolean; +} + +type Pagination = boolean | PaginationOptions; + +interface SortingOptions { + sort: PropertySort; +} + +type Sorting = boolean | SortingOptions; + +type InMemoryTableProps = Omit< + EuiBasicTableProps, + 'pagination' | 'sorting' | 'noItemsMessage' +> & { + message?: ReactNode; + search?: Search; + pagination?: undefined; + sorting?: Sorting; + /** + * Set `allowNeutralSort` to false to force column sorting. Defaults to true. + */ + allowNeutralSort?: boolean; + onTableChange?: (nextValues: Criteria) => void; + executeQueryOptions?: { + defaultFields?: string[]; + isClauseMatcher?: (...args: any) => boolean; + explain?: boolean; + }; +}; + +type InMemoryTablePropsWithPagination = Omit< + InMemoryTableProps, + 'pagination' | 'onTableChange' +> & { + pagination: Pagination; + onTableChange?: (nextValues: CriteriaWithPagination) => void; +}; + +export type EuiInMemoryTableProps = CommonProps & + (InMemoryTableProps | InMemoryTablePropsWithPagination); + +interface State { + prevProps: { + items: T[]; + sortName: ReactNode; + sortDirection?: Direction; + }; + query: Query; + pageIndex: number; + pageSize?: number; + pageSizeOptions?: number[]; + sortName: ReactNode; + sortDirection?: Direction; + allowNeutralSort: boolean; + hidePerPageOptions: boolean | undefined; +} + +const getInitialQuery = (search: Search | undefined) => { + if (!search) { + return; + } + + const query = (search as EuiSearchBarProps).defaultQuery || ''; + return isString(query) ? EuiSearchBar.Query.parse(query) : query; +}; + +const getInitialPagination = (pagination: Pagination | undefined) => { + if (!pagination) { + return { + pageIndex: undefined, + pageSize: undefined, + }; + } + + const { + initialPageIndex = 0, + initialPageSize, + pageSizeOptions = paginationBarDefaults.pageSizeOptions, + hidePerPageOptions, + } = pagination as PaginationOptions; + + if ( + !hidePerPageOptions && + initialPageSize && + (!pageSizeOptions || !pageSizeOptions.includes(initialPageSize)) + ) { + throw new Error( + `EuiInMemoryTable received initialPageSize ${initialPageSize}, which wasn't provided within pageSizeOptions.` + ); + } + + const defaultPageSize = pageSizeOptions + ? pageSizeOptions[0] + : paginationBarDefaults.pageSizeOptions[0]; + + return { + pageIndex: initialPageIndex, + pageSize: initialPageSize || defaultPageSize, + pageSizeOptions, + hidePerPageOptions, + }; +}; + +function findColumnByProp( + columns: Array>, + prop: 'field' | 'name', + value: string +) { + for (let i = 0; i < columns.length; i++) { + const column = columns[i]; + if ( + (column as Record<'field' | 'name', keyof T | string | ReactNode>)[ + prop + ] === value + ) { + return column; + } + } +} + +function getInitialSorting( + columns: Array>, + sorting: Sorting | undefined +) { + if (!sorting || !(sorting as SortingOptions).sort) { + return { + sortName: undefined, + sortDirection: undefined, + }; + } + + const { + field: sortable, + direction: sortDirection, + } = (sorting as SortingOptions).sort; + + // sortable could be a column's `field` or its `name` + // for backwards compatibility `field` must be checked first + let sortColumn = findColumnByProp(columns, 'field', sortable); + if (sortColumn == null) { + sortColumn = findColumnByProp(columns, 'name', sortable); + } + + if (sortColumn == null) { + return { + sortName: undefined, + sortDirection: undefined, + }; + } + + const sortName = sortColumn.name; + + return { + sortName, + sortDirection, + }; +} + +export class EuiInMemoryTable extends Component< + EuiInMemoryTableProps, + State +> { + static defaultProps = { + responsive: true, + }; + + static getDerivedStateFromProps( + nextProps: EuiInMemoryTableProps, + prevState: State + ) { + if (nextProps.items !== prevState.prevProps.items) { + // We have new items because an external search has completed, so reset pagination state. + return { + prevProps: { + ...prevState.prevProps, + items: nextProps.items, + }, + pageIndex: 0, + }; + } + const { sortName, sortDirection } = getInitialSorting( + nextProps.columns, + nextProps.sorting + ); + if ( + sortName !== prevState.prevProps.sortName || + sortDirection !== prevState.prevProps.sortDirection + ) { + return { + sortName, + sortDirection, + }; + } + return null; + } + + constructor(props: EuiInMemoryTableProps) { + super(props); + + const { columns, search, pagination, sorting, allowNeutralSort } = props; + const { + pageIndex, + pageSize, + pageSizeOptions, + hidePerPageOptions, + } = getInitialPagination(pagination); + const { sortName, sortDirection } = getInitialSorting(columns, sorting); + + this.state = { + prevProps: { + items: props.items, + sortName, + sortDirection, + }, + query: getInitialQuery(search), + pageIndex: pageIndex || 0, + pageSize, + pageSizeOptions, + sortName, + sortDirection, + allowNeutralSort: allowNeutralSort === false ? false : true, + hidePerPageOptions, + }; + } + + onTableChange = ({ page, sort }: Criteria) => { + const { index: pageIndex, size: pageSize } = (page || {}) as { + index: number; + size: number; + }; + + let { field: sortName, direction: sortDirection } = (sort || {}) as { + field: keyof T; + direction: Direction; + }; + + // To keep backwards compatibility reportedSortName needs to be tracked separately + // from sortName; sortName gets stored internally while reportedSortName is sent to the callback + let reportedSortName = sortName; + + // EuiBasicTable returns the column's `field` if it exists instead of `name`, + // map back to `name` if this is the case + for (let i = 0; i < this.props.columns.length; i++) { + const column = this.props.columns[i]; + if ((column as EuiTableFieldDataColumnType).field === sortName) { + sortName = column.name as keyof T; + break; + } + } + + // Allow going back to 'neutral' sorting + if ( + this.state.allowNeutralSort && + this.state.sortName === sortName && + this.state.sortDirection === 'desc' && + sortDirection === 'asc' + ) { + sortName = '' as keyof T; + reportedSortName = '' as keyof T; + sortDirection = 'asc'; // Default sort direction. + } + + if (this.props.onTableChange) { + this.props.onTableChange({ + // @ts-ignore complex relationship between pagination's existance and criteria, the code logic ensures this is correctly maintained + page, + sort: { + field: reportedSortName, + direction: sortDirection, + }, + }); + } + + this.setState({ + pageIndex, + pageSize, + sortName, + sortDirection, + }); + }; + + onQueryChange = ({ query, queryText, error }: onChangeArgument) => { + if (isEuiSearchBarProps(this.props.search)) { + const search = this.props.search; + if (search.onChange) { + const shouldQueryInMemory = search.onChange({ + query, + queryText, + error, + }); + if (!shouldQueryInMemory) { + return; + } + } + } + + // Reset pagination state. + this.setState({ + query, + pageIndex: 0, + }); + }; + + renderSearchBar() { + const { search } = this.props; + if (search) { + let searchBarProps: EuiSearchBarProps = {}; + + if (isEuiSearchBarProps(search)) { + const { onChange, ..._searchBarProps } = search; + searchBarProps = _searchBarProps; + + if (searchBarProps.box && searchBarProps.box.schema === true) { + searchBarProps.box.schema = this.resolveSearchSchema(); + } + } + + return ; + } + } + + resolveSearchSchema() { + const { columns } = this.props; + return columns.reduce<{ + strict: boolean; + fields: Record; + }>( + (schema, column) => { + const { field, dataType } = column as EuiTableFieldDataColumnType; + if (field) { + const type = dataType || 'string'; + schema.fields[field as string] = { type }; + } + return schema; + }, + { strict: true, fields: {} } + ); + } + + getItemSorter() { + const { sortName, sortDirection } = this.state; + + const { columns } = this.props; + + const sortColumn = columns.find( + ({ name }) => name === sortName + ) as EuiTableFieldDataColumnType; + + if (sortColumn == null) { + // can't return a non-function so return a function that says everything is the same + return () => () => 0; + } + + const sortable = sortColumn.sortable; + + if (typeof sortable === 'function') { + return Comparators.value(sortable, Comparators.default(sortDirection)); + } + + return Comparators.property( + sortColumn.field as string, + Comparators.default(sortDirection) + ); + } + + getItems() { + const { executeQueryOptions } = this.props; + const { + prevProps: { items }, + } = this.state; + + if (!items.length) { + return { + items: [], + totalItemCount: 0, + }; + } + + const { query, sortName, pageIndex, pageSize } = this.state; + + const matchingItems = query + ? EuiSearchBar.Query.execute(query, items, executeQueryOptions) + : items; + + const sortedItems = sortName + ? matchingItems + .slice(0) // avoid mutating the source array + .sort(this.getItemSorter()) // sort, causes mutation + : matchingItems; + + const visibleItems = pageSize + ? (() => { + const startIndex = pageIndex * pageSize; + return sortedItems.slice( + startIndex, + Math.min(startIndex + pageSize, sortedItems.length) + ); + })() + : sortedItems; + + return { + items: visibleItems, + totalItemCount: matchingItems.length, + }; + } + + render() { + const { + columns, + loading, + message, + error, + selection, + isSelectable, + hasActions, + compressed, + pagination: hasPagination, + sorting: hasSorting, + itemIdToExpandedRowMap, + itemId, + rowProps, + cellProps, + items: _unuseditems, // eslint-disable-line no-unused-vars + search, // eslint-disable-line no-unused-vars + onTableChange, // eslint-disable-line no-unused-vars + executeQueryOptions, // eslint-disable-line no-unused-vars + allowNeutralSort, // eslint-disable-line no-unused-vars + ...rest + } = this.props; + + const { + pageIndex, + pageSize, + pageSizeOptions, + sortName, + sortDirection, + hidePerPageOptions, + } = this.state; + + const { items, totalItemCount } = this.getItems(); + + const pagination: PaginationBarType | undefined = !hasPagination + ? undefined + : { + pageIndex, + pageSize: pageSize || 1, + pageSizeOptions, + totalItemCount, + hidePerPageOptions, + }; + + // Data loaded from a server can have a default sort order which is meaningful to the + // user, but can't be reproduced with client-side sort logic. So we allow the table to display + // rows in the order in which they're initially loaded by providing an undefined sorting prop. + const sorting: EuiTableSortingType | undefined = !hasSorting + ? undefined + : { + sort: + !sortName && !sortDirection + ? undefined + : { + field: sortName as keyof T, + direction: sortDirection as Direction, + }, + allowNeutralSort: this.state.allowNeutralSort, + }; + + const searchBar = this.renderSearchBar(); + + const table = ( + // @ts-ignore complex relationship between pagination's existance and criteria, the code logic ensures this is correctly maintained + + ); + + if (!searchBar) { + return table; + } + + return ( +
+ {searchBar} + + {table} +
+ ); + } +} diff --git a/src/components/basic_table/index.js b/src/components/basic_table/index.js deleted file mode 100644 index f3355d4295d..00000000000 --- a/src/components/basic_table/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { EuiBasicTable } from './basic_table'; -export { EuiInMemoryTable } from './in_memory_table'; diff --git a/src/components/basic_table/index.ts b/src/components/basic_table/index.ts new file mode 100644 index 00000000000..ab9b83fb543 --- /dev/null +++ b/src/components/basic_table/index.ts @@ -0,0 +1,15 @@ +export { + EuiBasicTable, + EuiBasicTableProps, + EuiBasicTableColumn, +} from './basic_table'; +export { EuiInMemoryTable, EuiInMemoryTableProps } from './in_memory_table'; +export { + EuiTableDataType, + EuiTableFooterProps, + EuiTableFieldDataColumnType, + EuiTableComputedColumnType, + EuiTableActionsColumnType, + EuiTableSelectionType, + EuiTableSortingType, +} from './table_types'; diff --git a/src/components/basic_table/loading_table_body.test.js b/src/components/basic_table/loading_table_body.test.tsx similarity index 100% rename from src/components/basic_table/loading_table_body.test.js rename to src/components/basic_table/loading_table_body.test.tsx diff --git a/src/components/basic_table/loading_table_body.js b/src/components/basic_table/loading_table_body.tsx similarity index 61% rename from src/components/basic_table/loading_table_body.js rename to src/components/basic_table/loading_table_body.tsx index 5860f0c5f97..b720c4f7bc6 100644 --- a/src/components/basic_table/loading_table_body.js +++ b/src/components/basic_table/loading_table_body.tsx @@ -1,14 +1,18 @@ import React, { Component } from 'react'; -import { EuiTableBody } from '../table/table_body'; +import { EuiTableBody } from '../table'; -export class LoadingTableBody extends Component { - constructor(props) { +export class LoadingTableBody extends Component<{}> { + private cleanups: Array<() => void>; + private tbody: HTMLTableSectionElement | null; + + constructor(props: {}) { super(props); this.cleanups = []; + this.tbody = null; } componentDidMount() { - const listener = event => { + const listener = (event: Event) => { event.stopPropagation(); event.preventDefault(); }; @@ -25,8 +29,10 @@ export class LoadingTableBody extends Component { 'keyup', 'keypress', ].forEach(event => { - this.tbody.addEventListener(event, listener, true); - this.cleanups.push(() => this.tbody.removeEventListener(event, listener)); + this.tbody!.addEventListener(event, listener, true); + this.cleanups.push(() => + this.tbody!.removeEventListener(event, listener) + ); }); } diff --git a/src/components/basic_table/pagination_bar.test.js b/src/components/basic_table/pagination_bar.test.tsx similarity index 100% rename from src/components/basic_table/pagination_bar.test.js rename to src/components/basic_table/pagination_bar.test.tsx diff --git a/src/components/basic_table/pagination_bar.js b/src/components/basic_table/pagination_bar.tsx similarity index 65% rename from src/components/basic_table/pagination_bar.js rename to src/components/basic_table/pagination_bar.tsx index 321acc99894..f5c6408f164 100644 --- a/src/components/basic_table/pagination_bar.js +++ b/src/components/basic_table/pagination_bar.tsx @@ -1,14 +1,24 @@ import React from 'react'; import { EuiSpacer } from '../spacer'; import { EuiTablePagination } from '../table'; -import PropTypes from 'prop-types'; +import { + ItemsPerPageChangeHandler, + PageChangeHandler, +} from '../table/table_pagination/table_pagination'; -export const PaginationType = PropTypes.shape({ - pageIndex: PropTypes.number.isRequired, - pageSize: PropTypes.number.isRequired, - totalItemCount: PropTypes.number.isRequired, - pageSizeOptions: PropTypes.arrayOf(PropTypes.number), -}); +export interface Pagination { + pageIndex: number; + pageSize: number; + totalItemCount: number; + pageSizeOptions?: number[]; + hidePerPageOptions?: boolean; +} + +export interface PaginationBarProps { + pagination: Pagination; + onPageSizeChange: ItemsPerPageChangeHandler; + onPageChange: PageChangeHandler; +} export const defaults = { pageSizeOptions: [10, 25, 50], @@ -18,7 +28,7 @@ export const PaginationBar = ({ pagination, onPageSizeChange, onPageChange, -}) => { +}: PaginationBarProps) => { const pageSizeOptions = pagination.pageSizeOptions ? pagination.pageSizeOptions : defaults.pageSizeOptions; @@ -38,9 +48,3 @@ export const PaginationBar = ({
); }; - -PaginationBar.propTypes = { - pagination: PaginationType.isRequired, - onPageSizeChange: PropTypes.func.isRequired, - onPageChange: PropTypes.func.isRequired, -}; diff --git a/src/components/basic_table/table_types.ts b/src/components/basic_table/table_types.ts new file mode 100644 index 00000000000..6dd662dcbd7 --- /dev/null +++ b/src/components/basic_table/table_types.ts @@ -0,0 +1,80 @@ +import { ReactElement, ReactNode, TdHTMLAttributes } from 'react'; +import { Direction, HorizontalAlignment } from '../../services'; +import { Pagination } from './pagination_bar'; +import { Action } from './action_types'; +import { Primitive } from '../../services/sort/comparators'; +import { CommonProps } from '../common'; + +export type ItemId = string | ((item: T) => string); +export type EuiTableDataType = + | 'auto' + | 'string' + | 'number' + | 'boolean' + | 'date'; + +export interface EuiTableFooterProps { + items: T[]; + pagination?: Pagination; +} +export interface EuiTableFieldDataColumnType + extends CommonProps, + TdHTMLAttributes { + field: keyof T | string; // supports outer.inner key paths + name: ReactNode; + description?: string; + dataType?: EuiTableDataType; + width?: string; + sortable?: boolean | ((item: T) => Primitive); + isExpander?: boolean; + textOnly?: boolean; + align?: HorizontalAlignment; + truncateText?: boolean; + isMobileHeader?: boolean; + mobileOptions?: { + show?: boolean; + only?: boolean; + render?: (item: T) => ReactNode; + header?: boolean; + }; + hideForMobile?: boolean; + render?: (value: any, record: T) => ReactNode; + footer?: + | string + | ReactElement + | ((props: EuiTableFooterProps) => ReactNode); +} + +export interface EuiTableComputedColumnType + extends CommonProps, + TdHTMLAttributes { + render: (record: T) => ReactNode; + name?: ReactNode; + description?: string; + sortable?: (item: T) => Primitive; + width?: string; + truncateText?: boolean; + isExpander?: boolean; + align?: HorizontalAlignment; +} + +export interface EuiTableActionsColumnType { + actions: Array>; + name?: ReactNode; + description?: string; + width?: string; +} + +export interface EuiTableSortingType { + sort?: { + field: keyof T; + direction: Direction; + }; + allowNeutralSort?: boolean; +} + +export interface EuiTableSelectionType { + onSelectionChange?: (selection: T[]) => void; + selectable?: (item: T) => boolean; + selectableMessage?: (selectable: boolean, item: T) => string; +} diff --git a/src/components/search_bar/index.js b/src/components/search_bar/index.js index 9ad496ff6d8..a758d48a4d3 100644 --- a/src/components/search_bar/index.js +++ b/src/components/search_bar/index.js @@ -1,3 +1,6 @@ export { EuiSearchBar, QueryType, Query, Ast } from './search_bar'; export { SearchBoxConfigPropTypes } from './search_box'; export { SearchFiltersFiltersType } from './search_filters'; + +// TODO: Some related types are defined in basic_table/in_memory_table. +// Use and remove them when TypeScriptification is done. diff --git a/src/components/table/mobile/table_sort_mobile.tsx b/src/components/table/mobile/table_sort_mobile.tsx index 6aa695999a2..f4bac123bda 100644 --- a/src/components/table/mobile/table_sort_mobile.tsx +++ b/src/components/table/mobile/table_sort_mobile.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component, ReactNode, Key } from 'react'; import classNames from 'classnames'; import { EuiButtonEmpty } from '../../button/button_empty'; @@ -7,17 +7,28 @@ import { EuiContextMenuPanel } from '../../context_menu'; import { EuiI18n } from '../../i18n'; import { EuiTableSortMobileItem } from './table_sort_mobile_item'; -interface Props { +interface ItemProps { + name: ReactNode; + key?: Key; + onSort?: () => void; + isSorted?: boolean; + isSortAscending?: boolean; +} + +export interface EuiTableSortMobileProps { className?: string; anchorPosition?: PopoverAnchorPosition; - items?: any[]; + items?: ItemProps[]; } interface State { isPopoverOpen: boolean; } -export class EuiTableSortMobile extends Component { +export class EuiTableSortMobile extends Component< + EuiTableSortMobileProps, + State +> { state = { isPopoverOpen: false, }; diff --git a/src/components/table/table_header_cell.tsx b/src/components/table/table_header_cell.tsx index 330cbc9bc61..b0c8406e20b 100644 --- a/src/components/table/table_header_cell.tsx +++ b/src/components/table/table_header_cell.tsx @@ -72,11 +72,11 @@ export const EuiTableHeaderCell: FunctionComponent = ({ mobileOptions = { show: true, }, + width, // Soon to be deprecated for {...mobileOptions} isMobileHeader, hideForMobile, style, - width, ...rest }) => { const classes = classNames('euiTableHeaderCell', className, { diff --git a/src/services/index.ts b/src/services/index.ts index 435abe03e60..96ae2bc8296 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -56,8 +56,10 @@ export { toInitials } from './string'; export { PropertySortType, + PropertySort, SortDirectionType, SortDirection, + Direction, SortableProperties, Comparators, } from './sort'; diff --git a/src/services/sort/index.ts b/src/services/sort/index.ts index a370c9e908e..049dbbe1cde 100644 --- a/src/services/sort/index.ts +++ b/src/services/sort/index.ts @@ -1,4 +1,4 @@ export { SortableProperties } from './sortable_properties'; -export { SortDirectionType, SortDirection } from './sort_direction'; -export { PropertySortType } from './property_sort'; +export { SortDirectionType, SortDirection, Direction } from './sort_direction'; +export { PropertySortType, PropertySort } from './property_sort'; export { Comparators } from './comparators'; diff --git a/src/services/sort/property_sort.ts b/src/services/sort/property_sort.ts index f0310c95f05..ea58ca3c1cb 100644 --- a/src/services/sort/property_sort.ts +++ b/src/services/sort/property_sort.ts @@ -1,7 +1,12 @@ import PropTypes from 'prop-types'; -import { SortDirectionType } from './sort_direction'; +import { SortDirectionType, Direction } from './sort_direction'; export const PropertySortType = PropTypes.shape({ field: PropTypes.string.isRequired, direction: SortDirectionType.isRequired, }); + +export interface PropertySort { + field: string; + direction: Direction; +}