diff --git a/packages/patternfly-4/react-core/src/components/TextInput/TextInput.js b/packages/patternfly-4/react-core/src/components/TextInput/TextInput.js index 2f67095347b..42742a60abd 100644 --- a/packages/patternfly-4/react-core/src/components/TextInput/TextInput.js +++ b/packages/patternfly-4/react-core/src/components/TextInput/TextInput.js @@ -52,7 +52,7 @@ const defaultProps = { isDisabled: false, isReadOnly: false, type: 'text', - value: null, + value: undefined, onChange: () => undefined, 'aria-label': null }; diff --git a/packages/patternfly-4/react-core/src/components/TextInput/__snapshots__/TextInput.test.js.snap b/packages/patternfly-4/react-core/src/components/TextInput/__snapshots__/TextInput.test.js.snap index 5def5e5e6b1..2618c8e557d 100644 --- a/packages/patternfly-4/react-core/src/components/TextInput/__snapshots__/TextInput.test.js.snap +++ b/packages/patternfly-4/react-core/src/components/TextInput/__snapshots__/TextInput.test.js.snap @@ -23,7 +23,6 @@ exports[`disabled text input 1`] = ` readOnly={false} required={false} type="text" - value={null} /> `; diff --git a/packages/patternfly-4/react-table/internal/util.js b/packages/patternfly-4/react-table/internal/util.js new file mode 100644 index 00000000000..748a4e4db86 --- /dev/null +++ b/packages/patternfly-4/react-table/internal/util.js @@ -0,0 +1,8 @@ +// TODO: better sharing util components between modules +export function debounce(func, wait) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; +} diff --git a/packages/patternfly-4/react-table/src/components/CancelButton/CancelButton.js b/packages/patternfly-4/react-table/src/components/CancelButton/CancelButton.js new file mode 100644 index 00000000000..8d0ea9c869d --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/CancelButton/CancelButton.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { CloseIcon } from '@patternfly/react-icons'; +import { Button } from '@patternfly/react-core'; + +const CancelButton = props => ( + +); + +CancelButton.propTypes = { + ...Button.propTypes +}; + +CancelButton.defaultProps = { + ...Button.defaultProps, + variant: 'plain' +}; + +export default CancelButton; diff --git a/packages/patternfly-4/react-table/src/components/CancelButton/CancelButton.test.js b/packages/patternfly-4/react-table/src/components/CancelButton/CancelButton.test.js new file mode 100644 index 00000000000..93336044229 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/CancelButton/CancelButton.test.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { CancelButton } from './index'; + +test('it renders properly', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); diff --git a/packages/patternfly-4/react-table/src/components/CancelButton/__snapshots__/CancelButton.test.js.snap b/packages/patternfly-4/react-table/src/components/CancelButton/__snapshots__/CancelButton.test.js.snap new file mode 100644 index 00000000000..95184fc1bc9 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/CancelButton/__snapshots__/CancelButton.test.js.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it renders properly 1`] = ` + +`; diff --git a/packages/patternfly-4/react-table/src/components/CancelButton/index.js b/packages/patternfly-4/react-table/src/components/CancelButton/index.js new file mode 100644 index 00000000000..c4dc29a593a --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/CancelButton/index.js @@ -0,0 +1 @@ +export { default as CancelButton } from './CancelButton'; diff --git a/packages/patternfly-4/react-table/src/components/ConfirmButton/ConfirmButton.js b/packages/patternfly-4/react-table/src/components/ConfirmButton/ConfirmButton.js new file mode 100644 index 00000000000..25be8e0353f --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/ConfirmButton/ConfirmButton.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { CheckIcon } from '@patternfly/react-icons'; +import { Button } from '@patternfly/react-core'; + +const ConfirmButton = props => ( + +); + +ConfirmButton.propTypes = { + ...Button.propTypes +}; + +ConfirmButton.defaultProps = { + ...Button.defaultProps, + variant: 'primary' +}; + +export default ConfirmButton; diff --git a/packages/patternfly-4/react-table/src/components/ConfirmButton/ConfirmButton.test.js b/packages/patternfly-4/react-table/src/components/ConfirmButton/ConfirmButton.test.js new file mode 100644 index 00000000000..2cd4b86d46e --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/ConfirmButton/ConfirmButton.test.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { ConfirmButton } from './index'; + +test('it renders properly', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); diff --git a/packages/patternfly-4/react-table/src/components/ConfirmButton/__snapshots__/ConfirmButton.test.js.snap b/packages/patternfly-4/react-table/src/components/ConfirmButton/__snapshots__/ConfirmButton.test.js.snap new file mode 100644 index 00000000000..c8d70e4c57a --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/ConfirmButton/__snapshots__/ConfirmButton.test.js.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it renders properly 1`] = ` + +`; diff --git a/packages/patternfly-4/react-table/src/components/ConfirmButton/index.js b/packages/patternfly-4/react-table/src/components/ConfirmButton/index.js new file mode 100644 index 00000000000..4ccccc70087 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/ConfirmButton/index.js @@ -0,0 +1 @@ +export { default as ConfirmButton } from './ConfirmButton'; diff --git a/packages/patternfly-4/react-table/src/components/Table/Body.js b/packages/patternfly-4/react-table/src/components/Table/Body.js index cbadd6b59e7..e9cbc18dcda 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Body.js +++ b/packages/patternfly-4/react-table/src/components/Table/Body.js @@ -8,7 +8,7 @@ const propTypes = { className: PropTypes.string, /** Specify key which should be used for labeling each row. */ rowKey: PropTypes.string, - /** Function that is fired when user clicks on row. */ + /** Function that is fired when user clicks on a row if not editing. */ onRowClick: PropTypes.func }; @@ -18,37 +18,70 @@ const defaultProps = { onRowClick: () => undefined }; -class ContextBody extends React.Component { - onRow = (row, props) => { - const { onRowClick } = this.props; - return ({ - isExpanded: row.isExpanded, - isOpen: row.isOpen, - onClick: (event) => onRowClick(event, row, props) - }); +const onMouseDown = (event, row, rowProps, { onRowClick, editConfig }) => { + const cell = event.target.closest('[data-key]'); + const cellNumber = parseInt(cell && cell.getAttribute('data-key')); + const hasCellNumber = !Number.isNaN(cellNumber); + + let onEditCellChanged; + let targetsAlreadyEditedCell = false; + + if (hasCellNumber && editConfig && typeof editConfig.onEditCellChanged === 'function') { + targetsAlreadyEditedCell = cellNumber === row.activeEditCell; + onEditCellChanged = () => { + editConfig.onEditCellChanged(event, row, { + rowIndex: rowProps.rowIndex, + columnIndex: cellNumber + }); + }; } + // give priority to fire onChange/onBlur callbacks + + setTimeout(() => { + if (!row.isEditing) { + onRowClick(event, row, rowProps); + if (onEditCellChanged) { + // edit cell after rerender + setTimeout(onEditCellChanged, 0); + } + } else if (onEditCellChanged && !targetsAlreadyEditedCell) { + onEditCellChanged(); + } + }, 0); +}; + +class ContextBody extends React.Component { + onRow = (row, rowProps) => ({ + row, + rowProps, + onMouseDown: event => onMouseDown(event, row, rowProps, this.props) + }); + parentsExpanded(parentId) { const { rows } = this.props; return rows[parentId].hasOwnProperty('parent') ? this.parentsExpanded(rows[parentId].parent) : rows[parentId].isOpen; } render() { - const { className, headerData, rows, rowKey, children, onRowClick, ...props } = this.props; + const { className, headerData, rows, rowKey, children, editConfig, onRowClick, ...props } = this.props; let shiftKey = 0; shiftKey += headerData[0] && headerData[0].extraParams.onSelect ? 1 : 0; shiftKey += headerData[0] && headerData[0].extraParams.onCollapse ? 1 : 0; + const isTableEditing = rows.some(oneRow => oneRow.isEditing); + const mappedRows = headerData.length !== 0 && rows.map((oneRow, oneRowKey) => { return { ...oneRow, ...oneRow && (oneRow.cells || oneRow).reduce( (acc, curr, key) => { + const isCurrObject = curr === Object(curr); return ({ ...acc, ...{ [headerData[shiftKey + key].property]: { - title: curr.title || curr, - props: curr.props + title: isCurrObject ? curr.title : curr, + props: isCurrObject ? curr.props : undefined } } }) @@ -57,6 +90,10 @@ class ContextBody extends React.Component { ...oneRow.parent !== undefined ? { isExpanded: this.parentsExpanded(oneRow.parent) && rows[oneRow.parent].isOpen } : {}, + isFirst: oneRowKey === 0, + isLast: oneRowKey === rows.length -1, + editConfig, + isTableEditing, } }); return ( @@ -69,7 +106,7 @@ class ContextBody extends React.Component { const TableBody = props => ( - {({ headerData, rows }) => } + {({ headerData, editConfig, rows }) => } ) diff --git a/packages/patternfly-4/react-table/src/components/Table/RowWrapper.js b/packages/patternfly-4/react-table/src/components/Table/RowWrapper.js index 0fdaaa11c16..a869a33b7a0 100644 --- a/packages/patternfly-4/react-table/src/components/Table/RowWrapper.js +++ b/packages/patternfly-4/react-table/src/components/Table/RowWrapper.js @@ -1,24 +1,203 @@ import React from 'react'; +import { createPortal } from 'react-dom'; import PropTypes from 'prop-types'; import { tableExpandableRow, modifiers } from '@patternfly/patternfly-next/components/Table/table.css'; import { css } from '@patternfly/react-styles'; +import { debounce } from '../../../internal/util'; +import TableConfirmButtonsRow from './TableConfirmButtonsRow'; +import { TableEditConfirmation } from './inlineEditConstants'; -const RowWrapper = ({ isOpen, isExpanded, ...props }) => ( - -); +const tableConfirmationMapper = { + [TableEditConfirmation.TABLE_TOP]: { + hasConfirmationButtons: ({ isTableEditing, isFirst }) => isTableEditing && isFirst, + isTableConfirmation: () => true, + areButtonsOnTop: () => true, + getEditStyles: ({ isFirst }) => + isFirst + ? { + borderTop: '2px solid #7dc3e8' + } + : null + }, + [TableEditConfirmation.TABLE_BOTTOM]: { + hasConfirmationButtons: ({ isTableEditing, isLast }) => isTableEditing && isLast, + isTableConfirmation: () => true, + areButtonsOnTop: () => false, + getEditStyles: () => null + }, + [TableEditConfirmation.ROW]: { + hasConfirmationButtons: ({ isEditing }) => isEditing, + isTableConfirmation: () => false, + areButtonsOnTop: ({ isLast }) => isLast, + getEditStyles: ({ isEditing }) => + isEditing + ? { + background: '#def3ff', + borderTopColor: '#7dc3e8', + borderBottomColor: '#7dc3e8' + } + : null + }, + [TableEditConfirmation.NONE]: { + hasConfirmationButtons: () => false, + isTableConfirmation: () => false, + areButtonsOnTop: () => false, + getEditStyles: () => null + } +}; + +const getTableConfirmation = ({ editConfig }) => + tableConfirmationMapper[editConfig && editConfig.editConfirmationType] || + tableConfirmationMapper[TableEditConfirmation.NONE]; + +class RowWrapper extends React.Component { + constructor(props) { + super(props); + this.state = {}; + + if (getTableConfirmation(this.props.row).hasConfirmationButtons(this.props.row)) { + this.handleScroll = debounce(this.handleScroll, 100); + this.handleResize = debounce(this.handleResize, 100); + } + } + + componentDidMount() { + this._unmounted = false; + if (getTableConfirmation(this.props.row).hasConfirmationButtons(this.props.row)) { + this.fetchClientDimensions(); + window.addEventListener('scroll', this.handleScroll); + window.addEventListener('resize', this.handleResize); + } + } + + componentWillUnmount() { + this._unmounted = true; + if (getTableConfirmation(this.props.row).hasConfirmationButtons(this.props.row)) { + window.removeEventListener('scroll', this.handleScroll); + window.removeEventListener('resize', this.handleResize); + } + } + + saveRowDimensions = element => { + if (element) { + this.element = element; + } + + if (this.element && !this._unmounted) { + this.setState({ + rowDimensions: this.element.getBoundingClientRect() + }); + } + }; + + handleScroll = event => { + this.saveRowDimensions(); + }; + + handleResize = event => { + this.fetchClientDimensions(); + this.saveRowDimensions(); + }; + + fetchClientDimensions() { + if (!this._unmounted) { + this.setState({ + window: { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight + } + }); + } + } + + getConfirmationButtons() { + const { row, rowProps, ...props } = this.props; + const { isOpen, isExpanded, isLast, isFirst, isEditing, isTableEditing, editConfig } = row; + + if (!editConfig) { + return null; + } + const { onEditConfirmed, onEditCanceled } = editConfig; + const tableConfirmation = getTableConfirmation(row); + + let confirmButtons; + if ( + tableConfirmation.hasConfirmationButtons({ isFirst, isLast, isEditing, isTableEditing }) && + this.element && + this.state.rowDimensions + ) { + const options = tableConfirmation.isTableConfirmation() ? {} : rowProps; + const actionObject = tableConfirmation.isTableConfirmation() ? null : row; + confirmButtons = createPortal( + onEditConfirmed(event, actionObject, options)} + onCancel={event => onEditCanceled(event, actionObject, options)} + buttonsOnTop={tableConfirmation.areButtonsOnTop({ isLast })} + environment={{ window: this.state.window, row: this.element.getBoundingClientRect() }} + />, + this.element.closest('table').parentNode + ); + } + return confirmButtons; + } + + render() { + const { + row: { isOpen, isExpanded, isLast, isFirst, isEditing, isTableEditing, editConfig }, + rowProps, + ...props + } = this.props; + + // TODO move style to pf-next + const rowStyle = { + ...getTableConfirmation({ editConfig }).getEditStyles({ + isEditing, + isTableEditing, + isFirst, + isLast + }) + }; + + return ( + + + {this.getConfirmationButtons()} + + ); + } +} RowWrapper.propTypes = { - isOpen: PropTypes.bool, - isExpanded: PropTypes.bool + row: PropTypes.shape({ + isOpen: PropTypes.bool, + isExpanded: PropTypes.bool, + isEditing: PropTypes.bool, + isTableEditing: PropTypes.bool, + isLast: PropTypes.bool, + isFirst: PropTypes.bool, + editConfig: PropTypes.object + }), + rowProps: PropTypes.object }; RowWrapper.defaultProps = { - isOpen: undefined, - isExpanded: undefined + row: { + isOpen: undefined, + isExpanded: undefined, + isEditing: undefined, + isTableEditing: undefined, + isLast: undefined, + isFirst: undefined, + editConfig: undefined + }, + rowProps: null }; export default RowWrapper; diff --git a/packages/patternfly-4/react-table/src/components/Table/Table.docs.js b/packages/patternfly-4/react-table/src/components/Table/Table.docs.js index 8013c60ae28..0a79d796d6f 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Table.docs.js +++ b/packages/patternfly-4/react-table/src/components/Table/Table.docs.js @@ -2,6 +2,8 @@ import { Table, TableHeader, TableBody } from '@patternfly/react-table'; import Simple from './examples/SimpleTable'; import Sortable from './examples/SortableTable'; import Selectable from './examples/SelectableTable'; +import Editable from './examples/EditableTable'; +import EditableTableColumn from './examples/EditableTableColumn'; import Actions from './examples/ActionsTable'; import CellHeader from './examples/CellHeader'; import Compact from './examples/CompactTable'; @@ -23,6 +25,8 @@ export default { { component: CellHeader, title: 'First cell as Header' }, { component: Compact, title: 'Compact Table' }, { component: Width, title: 'Table with Width Modifiers' }, - { component: Collapsible, title: 'Collapsible table' } + { component: Collapsible, title: 'Collapsible table' }, + { component: Editable, title: 'Editable table With Inline Edit Row' }, + { component: EditableTableColumn, title: 'Editable Table With Inline Edit Columns' } ] }; diff --git a/packages/patternfly-4/react-table/src/components/Table/Table.js b/packages/patternfly-4/react-table/src/components/Table/Table.js index faff2caf3b9..34315109a00 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Table.js +++ b/packages/patternfly-4/react-table/src/components/Table/Table.js @@ -10,6 +10,7 @@ import HeaderCell from './HeaderCell'; import RowWrapper from './RowWrapper'; import BodyWrapper from './BodyWrapper'; import { calculateColumns } from './utils/headerUtils'; +import { TableEditConfirmation } from './inlineEditConstants'; export const TableGridBreakpoint = { grid: 'grid', @@ -80,6 +81,12 @@ const propTypes = { }) ]) ).isRequired, + editConfig: PropTypes.shape({ + editConfirmationType: PropTypes.oneOf(Object.values(TableEditConfirmation)), + onEditCellChanged: PropTypes.func, + onEditConfirmed: PropTypes.func, + onEditCanceled: PropTypes.func + }), /** Aria labeled by this property collapse and select. */ rowLabeledBy: PropTypes.string, /** Id prefix for expand buttons. */ @@ -123,7 +130,8 @@ const defaultProps = { contentId: 'expanded-content', dropdownPosition: DropdownPosition.right, dropdownDirection: DropdownDirection.down, - gridBreakPoint: TableGridBreakpoint.gridMd + gridBreakPoint: TableGridBreakpoint.gridMd, + editConfig: null }; export const TableContext = React.createContext(); @@ -155,6 +163,7 @@ class Table extends React.Component { variant, rows, cells, + editConfig, ...props } = this.props; @@ -175,7 +184,8 @@ class Table extends React.Component { {header} diff --git a/packages/patternfly-4/react-table/src/components/Table/TableConfirmButtonsRow.js b/packages/patternfly-4/react-table/src/components/Table/TableConfirmButtonsRow.js new file mode 100644 index 00000000000..d7feffe3f1a --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/TableConfirmButtonsRow.js @@ -0,0 +1,98 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { CancelButton } from '../CancelButton'; +import { ConfirmButton } from '../ConfirmButton'; + +const buttonsTopPosition = (window, rowDimensions) => ({ + bottom: window.height - rowDimensions.top - 1, + right: window.width - rowDimensions.right + 10 +}); + +const buttonsBottomPosition = (window, rowDimensions) => ({ + top: rowDimensions.bottom - 1, + right: window.width - rowDimensions.right + 10 +}); + +const TableConfirmButtonsRow = ({ + messages: { confirmButtonLabel, cancelButtonLabel }, + onConfirm, + onCancel, + environment, + buttonsOnTop +}) => { + if (environment == null) { + return null; + } + const { window, row } = environment; + + const positionStyle = buttonsOnTop ? buttonsTopPosition(window, row) : buttonsBottomPosition(window, row); + + // TODO move style to pf-next (inline-edit-buttons) + const allStyles = { + ...positionStyle, + position: 'fixed', + zIndex: 1000, + background: '#def3ff', + border: '1px solid #7dc3e8', + margin: 0, + padding: '4px' + }; + + if (buttonsOnTop) { + allStyles.borderBottom = 0; + } else { + allStyles.borderTop = 0; + } + + const cancelButtonStyle = { + marginLeft: '4px' + }; + + return ( +
+ + +
+ ); +}; + +TableConfirmButtonsRow.defaultProps = { + onConfirm: () => undefined, + onCancel: () => undefined, + buttonsOnTop: false, + environment: undefined, + buttonsClassName: '', + messages: { + confirmButtonLabel: 'Save', + cancelButtonLabel: 'Cancel' + } +}; + +TableConfirmButtonsRow.propTypes = { + /** Confirm edit callback */ + onConfirm: PropTypes.func, + /** Cancel edit callback */ + onCancel: PropTypes.func, + /** Inject confirm buttons positions */ + environment: PropTypes.shape({ + window: PropTypes.shape({ + width: PropTypes.number, + height: PropTypes.number + }), + row: PropTypes.shape({ + top: PropTypes.number, + bottom: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number + }) + }), + buttonsOnTop: PropTypes.bool, + /** Additional confirm buttons classes */ + buttonsClassName: PropTypes.string, + messages: PropTypes.shape({ + confirmButtonLabel: PropTypes.string, + cancelButtonLabel: PropTypes.string + }) +}; + +export default TableConfirmButtonsRow; diff --git a/packages/patternfly-4/react-table/src/components/Table/__snapshots__/Table.test.js.snap b/packages/patternfly-4/react-table/src/components/Table/__snapshots__/Table.test.js.snap index 91771b2c40d..82145f8d620 100644 --- a/packages/patternfly-4/react-table/src/components/Table/__snapshots__/Table.test.js.snap +++ b/packages/patternfly-4/react-table/src/components/Table/__snapshots__/Table.test.js.snap @@ -377,6 +377,7 @@ exports[`Actions table 1`] = ` contentId="expanded-content" dropdownDirection="down" dropdownPosition="right" + editConfig={null} expandId="expandable-toggle" gridBreakPoint="grid-md" onCollapse={null} @@ -482,6 +483,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -536,6 +538,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -589,6 +592,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -642,6 +646,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -695,6 +700,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -749,6 +755,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -848,6 +855,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -902,6 +910,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -955,6 +964,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -1008,6 +1018,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -1061,6 +1072,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -1115,6 +1127,7 @@ exports[`Actions table 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": Array [ Object { @@ -1304,6 +1317,7 @@ exports[`Actions table 1`] = ` > ( + this.onChange(newValue, { rowIndex, columnIndex })} + autoFocus={autoFocus} + /> + ) + }); + + const workspacesFormatter = inlineEditFormatterFactory({ + resolveValue: (value, { rowData }) => rowData.data.workspace, + renderEdit: (workspace, { column, rowData, columnIndex, rowIndex }) => { + const dropdownItems = column.data.dropdownItems.map(item => {item}); + return ( + + this.onWorkspaceChange({ selected: event.target.text, isDropdownOpen: false }, { rowIndex, columnIndex }) + } + toggle={ + + this.onWorkspaceChange({ isDropdownOpen: !workspace.isDropdownOpen }, { rowIndex, columnIndex }) + } + > + {workspace.selected} + + } + isOpen={workspace.isDropdownOpen} + dropdownItems={dropdownItems} + /> + ); + }, + renderValue: workspace => workspace.selected + }); + + const privateRepoFormatter = inlineEditFormatterFactory({ + resolveValue: (value, { rowData }) => rowData.data.privateRepo, + renderEdit: (privateRepo, { columnIndex, rowIndex }) => ( + this.onPrivateRepoChange(value, { rowIndex, columnIndex })} + aria-label="checkbox" + /> + ), + renderValue: (privateRepo, { columnIndex, rowIndex }) => ( + + ) + }); + + this.state = { + columns: [ + { + title: 'Repositories', + cellFormatters: [textInputFormatter] + }, + { + title: 'Branches', + cellFormatters: [(value, { rowData }) => rowData.data.branches[rowData.data.branches.length - 1]] + }, + 'Pull requests', + { + title: 'Workspaces', + cellFormatters: [workspacesFormatter], + data: { + dropdownItems: ['Green', 'Purple', 'Orange', 'Grey'] + } + }, + { + title: 'Last Commit', + cellFormatters: [textInputFormatter] + }, + { + title: 'Private', + cellFormatters: [privateRepoFormatter] + } + ], + rows: [ + { + cells: ['one', null, 7, null, 'five'], + data: { + branches: ['master'], + workspace: { + selected: 'Green', + isDropdownOpen: false + }, + privateRepo: false + } + // isEditing: true, + // activeEditCell: 3 + }, + { + cells: ['', null, 0, null, ''], + data: { + branches: ['master', 'v0.7.0', 'v1.0.0'], + workspace: { + selected: 'Grey', + isDropdownOpen: false + }, + privateRepo: false + } + }, + { + cells: ['p', null, 0, null, ''], + data: { + branches: ['master', 'v0.7.0'], + workspace: { + selected: 'Orange', + isDropdownOpen: false + }, + privateRepo: true + } + } + ], + actions: [ + { + title: 'Edit', + onClick: this.onEditActionClick + } + ], + editedRowBackup: null + }; + } + + onPrivateRepoChange = (value, { rowIndex }) => { + this.setState(({ rows }) => { + const row = rows[rowIndex]; + row.data.privateRepo = value; + return { rows }; + }); + }; + + onWorkspaceChange = (value, { rowIndex }) => { + this.setState(({ rows }) => { + const row = rows[rowIndex]; + row.data.workspace = Object.assign({}, row.data.workspace, value); + return { rows }; + }); + }; + + onChange = (value, { rowIndex, columnIndex }) => { + this.setState(({ rows }) => { + rows = [...rows]; + const row = rows[rowIndex]; + row.cells[columnIndex] = value; + row.activeEditCell = null; // stop autoFocus + return { rows }; + }); + }; + + onEditCellChanged = (event, clickedRow, { rowIndex, columnIndex }) => { + const WORKSPACE_COL = 3; + const PRIVATE_REPO_COL = 5; + + if (clickedRow.isEditing) { + this.setState(({ rows }) => ({ + rows: rows.map((row, id) => { + row.activeEditCell = id === rowIndex ? columnIndex : null; + row.data.workspace.isDropdownOpen = id === rowIndex && columnIndex === WORKSPACE_COL; + if (id === rowIndex && columnIndex === PRIVATE_REPO_COL) { + row.data.privateRepo = !row.data.privateRepo; + } + return row; + }) + })); + } + }; + + onEditActionClick = (event, rowId) => { + this.setState( + ({ rows, editedRowBackup }) => + !editedRowBackup && { + editedRowBackup: JSON.parse(JSON.stringify(rows[rowId])), // clone + rows: rows.map((row, id) => { + row.isEditing = id === rowId; + return row; + }) + } + ); + }; + + onEditConfirmed = (event, clickedRow, { rowIndex }) => { + this.setState(({ rows }) => { + rows = [...rows]; + rows[rowIndex].isEditing = false; + return { + rows, + editedRowBackup: null, + activeEditCell: null + }; + }); + }; + + onEditCanceled = (event, clickedRow, { rowIndex }) => { + this.setState(({ rows, editedRowBackup }) => { + rows = [...rows]; + rows[rowIndex] = editedRowBackup; + return { + rows, + editedRowBackup: null, + activeEditCell: null + }; + }); + }; + + render() { + const { columns, rows, actions, editedRowBackup } = this.state; + const editConfig = { + onEditCellChanged: this.onEditCellChanged, + editConfirmationType: TableEditConfirmation.ROW, + onEditConfirmed: this.onEditConfirmed, + onEditCanceled: this.onEditCanceled + }; + + const isTableEditing = !!editedRowBackup; + const mappedActions = actions.map(action => { + action.isDisabled = isTableEditing; + return action; + }); + + return ( + + + +
+ ); + } +} + +export default EditableTable; diff --git a/packages/patternfly-4/react-table/src/components/Table/examples/EditableTableColumn.js b/packages/patternfly-4/react-table/src/components/Table/examples/EditableTableColumn.js new file mode 100644 index 00000000000..5ae47531fcc --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/examples/EditableTableColumn.js @@ -0,0 +1,133 @@ +/* eslint-disable react/no-unused-state */ +import React from 'react'; +import { + Table, + TableHeader, + TableBody, + inlineEditFormatterFactory, + TableEditConfirmation, + TableTextInput +} from '@patternfly/react-table'; + +class EditableTableColumn extends React.Component { + constructor(props) { + super(props); + + const inlineEditingFormatter = inlineEditFormatterFactory({ + renderEdit: (value, { columnIndex, rowIndex }, { autoFocus }) => ( + this.onBlur(newValue, { rowIndex, columnIndex })} + autoFocus={autoFocus} + /> + ), + renderValue: (value, { rowData }) => (rowData.isTableEditing ? `${value} (Not Editable)` : value), + isEditable: ({ rowIndex }) => rowIndex !== 1 + }); + + this.state = { + columns: [ + { + title: 'Repositories', + cellFormatters: [inlineEditingFormatter] + }, + { + title: 'Branches', + cellFormatters: [inlineEditingFormatter] + }, + 'Pull requests', + 'Workspaces', + { + title: 'Last Commit', + cellFormatters: [inlineEditingFormatter] + } + ], + rows: [ + { + cells: ['one', 'two', 7, 'four', 'five'] + }, + { + cells: ['a', 'two', 0, 'four', 'five'] + }, + { + cells: ['p', 'two', 0, 'four', 'five'] + }, + { + cells: ['a', 'two', 5, 'four', 'five'] + } + ], + rowsBackup: null + }; + } + + onBlur = (value, { rowIndex, columnIndex }) => { + this.setState(({ rows }) => { + rows = [...rows]; + const row = rows[rowIndex]; + row.cells[columnIndex] = value; + row.activeEditCell = null; // stop autoFocus + return { rows }; + }); + }; + + onEditCellChanged = (event, clickedRow, { rowIndex, columnIndex }) => { + this.setState(({ rows }) => ({ + rows: rows.map((row, id) => { + row.activeEditCell = id === rowIndex ? columnIndex : null; + return row; + }) + })); + }; + + setEditing = (rows, isEditing) => + rows.map(row => { + row.isEditing = isEditing; + if (!isEditing) { + row.activeEditCell = null; + } + return row; + }); + + onRowClick = () => { + this.setState( + ({ rows, rowsBackup }) => + !rowsBackup && { + rowsBackup: JSON.parse(JSON.stringify(rows)), // clone + rows: this.setEditing(rows, true) + } + ); + }; + + onEditConfirmed = () => { + this.setState(({ rows }) => ({ + rows: this.setEditing(rows, false), + rowsBackup: null + })); + }; + + onEditCanceled = () => { + this.setState(({ rows, rowsBackup }) => ({ + rows: rowsBackup, + rowsBackup: null + })); + }; + + render() { + const { columns, rows } = this.state; + const editConfig = { + editConfirmationType: TableEditConfirmation.TABLE_TOP, + onEditCellChanged: this.onEditCellChanged, + onEditConfirmed: this.onEditConfirmed, + onEditCanceled: this.onEditCanceled + }; + + return ( + + + +
+ ); + } +} +export default EditableTableColumn; diff --git a/packages/patternfly-4/react-table/src/components/Table/index.js b/packages/patternfly-4/react-table/src/components/Table/index.js index 6b13631ff6c..95a7b14a330 100644 --- a/packages/patternfly-4/react-table/src/components/Table/index.js +++ b/packages/patternfly-4/react-table/src/components/Table/index.js @@ -1,5 +1,6 @@ export { default as Table, TableGridBreakpoint, TableVariant } from './Table'; +export { TableEditConfirmation } from './inlineEditConstants'; export { default as TableHeader } from './Header'; export { default as TableBody } from './Body'; -export { sortable, headerCol, cellWidth } from './utils'; -export { SortByDirection } from './SortColumn'; +export { sortable, inlineEditFormatterFactory, headerCol, cellWidth } from './utils'; +export { SortByDirection } from './SortColumn'; diff --git a/packages/patternfly-4/react-table/src/components/Table/inlineEditConstants.js b/packages/patternfly-4/react-table/src/components/Table/inlineEditConstants.js new file mode 100644 index 00000000000..04c5f306f9f --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/inlineEditConstants.js @@ -0,0 +1,7 @@ +// TODO: do edit buttons make sense on the left? +export const TableEditConfirmation = { + NONE: 'NONE', + ROW: 'ROW', + TABLE_TOP: 'TABLE_TOP', + TABLE_BOTTOM: 'TABLE_BOTTOM' +}; diff --git a/packages/patternfly-4/react-table/src/components/Table/utils/formatters.js b/packages/patternfly-4/react-table/src/components/Table/utils/formatters.js index ca7e435cd76..e2e6d4297ee 100644 --- a/packages/patternfly-4/react-table/src/components/Table/utils/formatters.js +++ b/packages/patternfly-4/react-table/src/components/Table/utils/formatters.js @@ -1 +1,3 @@ -export const defaultTitle = (data) => data && data.hasOwnProperty('title') ? data.title : data; +export { default as inlineEditFormatterFactory } from './formatters/inlineEditFormatterFactory'; + +export const defaultTitle = data => (data && data.hasOwnProperty('title') ? data.title : data); diff --git a/packages/patternfly-4/react-table/src/components/Table/utils/formatters/inlineEditFormatterFactory.js b/packages/patternfly-4/react-table/src/components/Table/utils/formatters/inlineEditFormatterFactory.js new file mode 100644 index 00000000000..371450f9f91 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/utils/formatters/inlineEditFormatterFactory.js @@ -0,0 +1,22 @@ +const inlineEditFormatterFactory = ({ renderEdit, renderValue, resolveValue, isEditable = null } = {}) => ( + value, + additionalData +) => { + const { rowData, columnIndex } = additionalData; + + if (resolveValue) { + value = resolveValue(value, additionalData); + } + + if (renderEdit && rowData.isEditing && (!isEditable || isEditable(additionalData))) { + return renderEdit(value, additionalData, { + autoFocus: rowData.activeEditCell === columnIndex + }); + } else if (renderValue) { + return renderValue(value, additionalData); + } + + return value; +}; + +export default inlineEditFormatterFactory; diff --git a/packages/patternfly-4/react-table/src/components/Table/utils/headerUtils.js b/packages/patternfly-4/react-table/src/components/Table/utils/headerUtils.js index cc90ad4c130..f35c645042d 100644 --- a/packages/patternfly-4/react-table/src/components/Table/utils/headerUtils.js +++ b/packages/patternfly-4/react-table/src/components/Table/utils/headerUtils.js @@ -67,6 +67,7 @@ const mapHeader = (column, extra, key, ...props) => { return ({ property: (typeof title === 'string' && title.toLowerCase().trim().replace(/\s/g, '-')) || `column-${key}`, extraParams: extra, + data: column.data, header: generateHeader(column, title), cell: generateCell(column, extra), props: { diff --git a/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.d.ts b/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.d.ts new file mode 100644 index 00000000000..9fcbdac0fdf --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.d.ts @@ -0,0 +1,13 @@ +import { SFC, FormEvent } from 'react'; +import { TextInputProps } from '@patternfly/react-core'; +import { Omit } from '../../../../react-core/src/typeUtils'; + +export interface TableTextInputProps extends Omit { + defaultValue?: string; + autoFocus?: boolean; + onBlur?(value: string, event: FormEvent): void; +} + +declare const TableTextInput: SFC; + +export default TableTextInput; diff --git a/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.docs.js b/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.docs.js new file mode 100644 index 00000000000..d6ba3611afd --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.docs.js @@ -0,0 +1,10 @@ +import { TableTextInput } from '@patternfly/react-table'; +import Simple from './examples/SimpleTableTextInput'; + +export default { + title: 'TableTextInput', + components: { + TableTextInput + }, + examples: [{component: Simple, title: 'Simple TableTextInput'}] +}; diff --git a/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.js b/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.js new file mode 100644 index 00000000000..9e16025d0b3 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.js @@ -0,0 +1,53 @@ +import React from 'react'; +// import styles from '@patternfly/patternfly-next/components/TableTextInput/styles.css'; +import { TextInput } from '@patternfly/react-core'; +import { css } from '@patternfly/react-styles'; +import PropTypes from 'prop-types'; + +const textInputProptypes = { ...TextInput.propTypes }; +const textInputDefaultProptypes = { ...TextInput.defaultProps }; +[textInputProptypes, textInputDefaultProptypes].forEach(types => { + ['value', 'onChange'].forEach(value => { + delete types[value]; + }); +}); + +const propTypes = { + ...textInputProptypes, + defaultValue: PropTypes.string, + onBlur: PropTypes.func, + autoFocus: PropTypes.bool +}; + +const defaultProps = { + ...textInputDefaultProptypes, + defaultValue: null, + onBlur: null, + autoFocus: false +}; + +class TableTextInput extends React.Component { + handleBlur = event => { + this.props.onBlur(event.currentTarget.value, event); + }; + + render() { + const { defaultValue, onBlur, autoFocus, value, onChange, ...textInputProps } = this.props; + return ( + + + + ); + } +} + +TableTextInput.propTypes = propTypes; +TableTextInput.defaultProps = defaultProps; + +export default TableTextInput; diff --git a/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.test.js b/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.test.js new file mode 100644 index 00000000000..8001fe45c54 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/TableTextInput/TableTextInput.test.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import TableTextInput from './TableTextInput'; + +const props = {}; + +test('simple table text input', () => { + const view = shallow(); + expect(view).toMatchSnapshot(); +}); diff --git a/packages/patternfly-4/react-table/src/components/TableTextInput/__snapshots__/TableTextInput.test.js.snap b/packages/patternfly-4/react-table/src/components/TableTextInput/__snapshots__/TableTextInput.test.js.snap new file mode 100644 index 00000000000..5303e0ea3ea --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/TableTextInput/__snapshots__/TableTextInput.test.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`simple table text input 1`] = ` + + + +`; diff --git a/packages/patternfly-4/react-table/src/components/TableTextInput/examples/SimpleTableTextInput.js b/packages/patternfly-4/react-table/src/components/TableTextInput/examples/SimpleTableTextInput.js new file mode 100644 index 00000000000..b7ede5eb5e6 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/TableTextInput/examples/SimpleTableTextInput.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { TableTextInput } from '@patternfly/react-table'; + +class SimpleTableTextInput extends React.Component { + state = { + value: '' + }; + + handleTextInputChange = value => { + this.setState({ value }); + }; + + render() { + const { value } = this.state; + + return ( + + ); + } +} + +export default SimpleTableTextInput; diff --git a/packages/patternfly-4/react-table/src/components/TableTextInput/index.js b/packages/patternfly-4/react-table/src/components/TableTextInput/index.js new file mode 100644 index 00000000000..eab02a44ac5 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/TableTextInput/index.js @@ -0,0 +1 @@ +export { default as TableTextInput } from './TableTextInput'; diff --git a/packages/patternfly-4/react-table/src/components/index.js b/packages/patternfly-4/react-table/src/components/index.js index 75193adc339..71934a49510 100644 --- a/packages/patternfly-4/react-table/src/components/index.js +++ b/packages/patternfly-4/react-table/src/components/index.js @@ -1 +1,2 @@ export * from './Table'; +export * from './TableTextInput';