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..247c6a5ff42 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 }; @@ -20,13 +20,44 @@ const defaultProps = { class ContextBody extends React.Component { onRow = (row, props) => { - const { onRowClick } = this.props; + const { onRowClick, editConfig } = this.props; return ({ - isExpanded: row.isExpanded, - isOpen: row.isOpen, - onClick: (event) => onRowClick(event, row, props) + row, + rowProps: props, + onMouseDown: (event) => { + 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: props.rowIndex, + columnIndex: cellNumber + }); + }; + } + + // give priority to fire onChange/onBlur callbacks + + setTimeout(() => { + if (!row.isEditing) { + onRowClick(event, row, props); + if (onEditCellChanged) { + // edit cell after rerender + setTimeout(onEditCellChanged, 0); + } + } else if (onEditCellChanged && !targetsAlreadyEditedCell) { + onEditCellChanged(); + } + }, 0); + } }); - } + }; parentsExpanded(parentId) { const { rows } = this.props; @@ -34,21 +65,24 @@ class ContextBody extends React.Component { } 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 +91,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 +107,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..f346ed82c11 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,228 @@ 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 './Table'; -const RowWrapper = ({ isOpen, isExpanded, ...props }) => ( - -); +const hasConfirmationButtons = (editConfirmationType, { isEditing, isTableEditing, isFirst, isLast }) => { + switch (editConfirmationType) { + case TableEditConfirmation.TABLE_TOP: + return isTableEditing && isFirst; + case TableEditConfirmation.TABLE_BOTTOM: + return isTableEditing && isLast; + case TableEditConfirmation.ROW: + return isEditing; + case TableEditConfirmation.NONE: + default: + return false; + } +}; + +export const areButtonsOnTop = (editConfirmationType, { isLast }) => { + switch (editConfirmationType) { + case TableEditConfirmation.TABLE_TOP: + return true; + case TableEditConfirmation.ROW: + return isLast; + case TableEditConfirmation.TABLE_BOTTOM: + case TableEditConfirmation.NONE: + default: + return false; + } +}; + +export const getEditStyles = (editConfirmationType, { isEditing, isTableEditing, isFirst, isLast }) => { + let editStyle; + + if (isTableEditing) { + switch (editConfirmationType) { + case TableEditConfirmation.ROW: + if (isEditing) { + editStyle = { + background: '#def3ff', + borderTopColor: '#7dc3e8', + borderBottomColor: '#7dc3e8' + }; + } + break; + case TableEditConfirmation.TABLE_TOP: + if (isFirst) { + editStyle = { + borderTop: '2px solid #7dc3e8' + }; + } + break; + default: + } + } + return editStyle; +}; + +export const isTableConfirmation = editConfirmationType => { + switch (editConfirmationType) { + case TableEditConfirmation.TABLE_TOP: + case TableEditConfirmation.TABLE_BOTTOM: + return true; + default: + return false; + } +}; + +class RowWrapper extends React.Component { + constructor(props) { + super(props); + this.state = {}; + + if (hasConfirmationButtons(this.getEditConfirmationType(), this.props.row)) { + this.handleScroll = debounce(this.handleScroll, 100); + this.handleResize = debounce(this.handleResize, 100); + } + } + + componentDidMount() { + this._unmounted = false; + if (hasConfirmationButtons(this.getEditConfirmationType(), this.props.row)) { + this.fetchClientDimensions(); + window.addEventListener('scroll', this.handleScroll); + window.addEventListener('resize', this.handleResize); + } + } + + componentWillUnmount() { + this._unmounted = true; + if (hasConfirmationButtons(this.getEditConfirmationType(), 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 + } + }); + } + } + + getEditConfirmationType() { + const { editConfig } = this.props.row; + return editConfig && editConfig.editConfirmationType; + } + + getConfirmationButtons() { + const { row, rowProps, ...props } = this.props; + const { isOpen, isExpanded, isLast, isFirst, isEditing, isTableEditing, editConfig } = row; + + if (!editConfig) { + return null; + } + const { editConfirmationType, onEditConfirmed, onEditCanceled } = editConfig; + + let confirmButtons; + if ( + hasConfirmationButtons(editConfirmationType, { isFirst, isLast, isEditing, isTableEditing }) && + this.element && + this.state.rowDimensions + ) { + const options = isTableConfirmation(editConfirmationType) ? {} : rowProps; + const actionObject = isTableConfirmation(editConfirmationType) ? null : row; + confirmButtons = createPortal( + onEditConfirmed(event, actionObject, options)} + onCancel={event => onEditCanceled(event, actionObject, options)} + buttonsOnTop={areButtonsOnTop(editConfirmationType, { 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 = { + ...getEditStyles(this.getEditConfirmationType(), { + 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..f45817e4439 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Table.js +++ b/packages/patternfly-4/react-table/src/components/Table/Table.js @@ -21,6 +21,13 @@ export const TableVariant = { compact: 'compact' }; +export const TableEditConfirmation = { + NONE: 'NONE', // TODO: do edit buttons make sense on the left? + ROW: 'ROW', + TABLE_TOP: 'TABLE_TOP', + TABLE_BOTTOM: 'TABLE_BOTTOM' +}; + const propTypes = { /** Table elements [Head, Body and Footer]. */ children: PropTypes.node, @@ -80,6 +87,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 +136,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 +169,7 @@ class Table extends React.Component { variant, rows, cells, + editConfig, ...props } = this.props; @@ -175,7 +190,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..ed332592ec9 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/TableConfirmButtonsRow.js @@ -0,0 +1,101 @@ +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 ( +
+ + +
+ ); +}; + +// TODO: shouldComponentUpdate? +// TableConfirmButtonsRow.shouldComponentUpdate = true; + +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..0aba1263981 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 @@ -63,15 +63,9 @@ exports[`Actions table 1`] = ` } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-dropdown__toggle.pf-m-plain { display: flex; @@ -94,15 +88,9 @@ exports[`Actions table 1`] = ` } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-dropdown__toggle.pf-m-plain { display: flex; @@ -125,15 +113,9 @@ exports[`Actions table 1`] = ` } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-dropdown__toggle.pf-m-plain { display: flex; @@ -156,15 +138,9 @@ exports[`Actions table 1`] = ` } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-dropdown__toggle.pf-m-plain { display: flex; @@ -187,15 +163,9 @@ exports[`Actions table 1`] = ` } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-dropdown__toggle.pf-m-plain { display: flex; @@ -218,15 +188,9 @@ exports[`Actions table 1`] = ` } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-dropdown__toggle.pf-m-plain { display: flex; @@ -249,15 +213,9 @@ exports[`Actions table 1`] = ` } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-dropdown__toggle.pf-m-plain { display: flex; @@ -280,15 +238,9 @@ exports[`Actions table 1`] = ` } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-dropdown__toggle.pf-m-plain { display: flex; @@ -311,22 +263,16 @@ exports[`Actions table 1`] = ` } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } .pf-c-table__action { display: block; - padding-top: 0px; - padding-bottom: 0px; - vertical-align: middle; } -.pf-c-table { +.pf-c-table.pf-m-grid-md { display: block; width: 100%; background-color: #ffffff; } -.pf-c-table { +.pf-c-table.pf-m-grid-md { display: block; width: 100%; background-color: #ffffff; @@ -377,6 +323,7 @@ exports[`Actions table 1`] = ` contentId="expanded-content" dropdownDirection="down" dropdownPosition="right" + editConfig={null} expandId="expandable-toggle" gridBreakPoint="grid-md" onCollapse={null} @@ -470,7 +417,7 @@ exports[`Actions table 1`] = ` >
@@ -34699,6 +38312,7 @@ exports[`Simple table caption 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": undefined, "contentId": "expanded-content", @@ -34734,6 +38348,7 @@ exports[`Simple table caption 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": undefined, "contentId": "expanded-content", @@ -34769,6 +38384,7 @@ exports[`Simple table caption 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": undefined, "contentId": "expanded-content", @@ -34804,6 +38420,7 @@ exports[`Simple table caption 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": undefined, "contentId": "expanded-content", @@ -34839,6 +38456,7 @@ exports[`Simple table caption 1`] = ` [Function], ], }, + "data": undefined, "extraParams": Object { "actions": undefined, "contentId": "expanded-content", @@ -34953,6 +38571,7 @@ exports[`Simple table caption 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..0834628ce4c 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,5 @@ -export { default as Table, TableGridBreakpoint, TableVariant } from './Table'; +export { default as Table, TableGridBreakpoint, TableVariant, TableEditConfirmation } from './Table'; 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/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..f58980868f4 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/utils/formatters/inlineEditFormatterFactory.js @@ -0,0 +1,26 @@ +const inlineEditFormatterFactory = ({ renderEdit, renderValue, resolveValue, isEditable = null } = {}) => ( + value, + additionalData +) => { + const { rowData, columnIndex } = additionalData; + + let result; + + if (resolveValue) { + value = resolveValue(value, additionalData); + } + + if (renderEdit && rowData.isEditing && (!isEditable || isEditable(additionalData))) { + result = renderEdit(value, additionalData, { + autoFocus: rowData.activeEditCell === columnIndex + }); + } else if (renderValue) { + result = renderValue(value, additionalData); + } else { + result = value; + } + + return result; +}; + +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..95fddd3a8fe --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/TableTextInput/__snapshots__/TableTextInput.test.js.snap @@ -0,0 +1,20 @@ +// 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';