diff --git a/packages/patternfly-4/react-core/build/copyStyles.js b/packages/patternfly-4/react-core/build/copyStyles.js
old mode 100644
new mode 100755
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 9130c743d38..f940b0a0ed9 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 b10bb69becd..c4b3c05a392 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-docs/build/copyDocs.js b/packages/patternfly-4/react-docs/build/copyDocs.js
index 7e4e9e588c3..0312f0888e1 100644
--- a/packages/patternfly-4/react-docs/build/copyDocs.js
+++ b/packages/patternfly-4/react-docs/build/copyDocs.js
@@ -5,7 +5,7 @@ const fs = require('fs-extra');
const dest = path.resolve(__dirname, '../dist');
-const packageDirs = ['react-core', 'react-charts', 'react-table', 'react-styled-system'];
+const packageDirs = ['react-core', 'react-charts', 'react-table', 'react-inline-edit-extension', 'react-styled-system'];
const moduleTypes = ['js', 'esm'];
moduleTypes.forEach(moduleType => {
diff --git a/packages/patternfly-4/react-docs/gatsby-config.js b/packages/patternfly-4/react-docs/gatsby-config.js
index 82fb6a428e8..554057cc2e3 100644
--- a/packages/patternfly-4/react-docs/gatsby-config.js
+++ b/packages/patternfly-4/react-docs/gatsby-config.js
@@ -40,6 +40,13 @@ module.exports = {
ignore: [`**/*.d.ts`]
}
},
+ {
+ resolve: `gatsby-source-filesystem`,
+ options: {
+ name: `components`,
+ path: resolve(__dirname, '../react-inline-edit-extension/src')
+ }
+ },
'gatsby-transformer-react-docgen'
],
pathPrefix: 'patternfly-4'
diff --git a/packages/patternfly-4/react-docs/gatsby-node.js b/packages/patternfly-4/react-docs/gatsby-node.js
index 4afce646396..254be200c8b 100644
--- a/packages/patternfly-4/react-docs/gatsby-node.js
+++ b/packages/patternfly-4/react-docs/gatsby-node.js
@@ -1,6 +1,6 @@
const path = require(`path`);
const fs = require('fs-extra'); //eslint-disable-line
-const packageDirs = ['react-core', 'react-charts', 'react-styled-system', 'react-table'];
+const packageDirs = ['react-core', 'react-charts', 'react-styled-system', 'react-table', 'react-inline-edit-extension'];
// Escape single quotes and backslashes in a file path
const escapeFilePath = filePath => filePath.replace(/[\\']/g, '\\$&');
@@ -28,7 +28,8 @@ exports.onCreateWebpackConfig = ({ stage, loaders, actions, plugins, getConfig }
'@patternfly/react-charts': path.resolve(__dirname, '../react-charts/src'),
'@patternfly/react-core': path.resolve(__dirname, '../react-core/src'),
'@patternfly/react-styles': path.resolve(__dirname, '../react-styles/src'),
- '@patternfly/react-styled-system': path.resolve(__dirname, '../react-styled-system/src')
+ '@patternfly/react-styled-system': path.resolve(__dirname, '../react-styled-system/src'),
+ '@patternfly/react-inline-edit-extension': path.resolve(__dirname, '../react-inline-edit-extension/src'),
}
}
});
@@ -119,7 +120,7 @@ exports.createPages = async ({ graphql, actions }) => {
const filePath = path.resolve(__dirname, '.tmp', doc.base);
const rawExamples = [];
- const packageDir = packageDirs.find(pkg => doc.absolutePath.indexOf(pkg) !== -1);
+ const packageDir = packageDirs.find(pkg => doc.absolutePath.indexOf(`/${pkg}/`) !== -1);
// In Windows environments, paths use backslashes to separate directories;
// Ensure that forward slashes are used to make it comparable
diff --git a/packages/patternfly-4/react-docs/package.json b/packages/patternfly-4/react-docs/package.json
index be4e695c9b6..2d5a0aa33dd 100644
--- a/packages/patternfly-4/react-docs/package.json
+++ b/packages/patternfly-4/react-docs/package.json
@@ -11,6 +11,7 @@
"@patternfly/react-charts": "^2.2.2",
"@patternfly/react-core": "^2.9.2",
"@patternfly/react-icons": "^3.6.1",
+ "@patternfly/react-inline-edit-extension": "^1.0.0",
"@patternfly/react-styled-system": "^2.0.17",
"@patternfly/react-styles": "^2.4.0",
"@patternfly/react-table": "^1.3.4",
diff --git a/packages/patternfly-4/react-docs/src/components/example/liveDemo.js b/packages/patternfly-4/react-docs/src/components/example/liveDemo.js
index 28391a0f212..fa5798604fb 100644
--- a/packages/patternfly-4/react-docs/src/components/example/liveDemo.js
+++ b/packages/patternfly-4/react-docs/src/components/example/liveDemo.js
@@ -4,6 +4,7 @@ import exampleStyles from './example.styles';
import styles from './liveDemo.styles';
import PropTypes from 'prop-types';
import * as TableComponents from '@patternfly/react-table';
+import * as TableInlineEditingComponents from '@patternfly/react-inline-edit-extension';
import * as ChartComponents from '@patternfly/react-charts';
import * as CoreComponents from '@patternfly/react-core';
import * as CoreIcons from '@patternfly/react-icons';
@@ -35,6 +36,7 @@ const scopePlayground = {
React,
...ChartComponents,
...TableComponents,
+ ...TableInlineEditingComponents,
...StyledSystemComponents,
...CoreComponents,
...CoreIcons,
diff --git a/packages/patternfly-4/react-inline-edit-extension/.babelrc b/packages/patternfly-4/react-inline-edit-extension/.babelrc
new file mode 100644
index 00000000000..ba0214495d7
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/.babelrc
@@ -0,0 +1,31 @@
+{
+ "presets": [
+ "../.babelrc.js"
+ ],
+ "env": {
+ "production:esm": {
+ "plugins": [
+ [
+ "@patternfly/react-styles/babel",
+ {
+ "srcDir": "./src",
+ "outDir": "./dist/esm",
+ "useModules": true
+ }
+ ]
+ ]
+ },
+ "production:cjs": {
+ "plugins": [
+ [
+ "@patternfly/react-styles/babel",
+ {
+ "srcDir": "./src",
+ "outDir": "./dist/js",
+ "useModules": false
+ }
+ ]
+ ]
+ }
+ }
+}
diff --git a/packages/patternfly-4/react-inline-edit-extension/.npmignore b/packages/patternfly-4/react-inline-edit-extension/.npmignore
new file mode 100644
index 00000000000..378eac25d31
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/.npmignore
@@ -0,0 +1 @@
+build
diff --git a/packages/patternfly-4/react-inline-edit-extension/README.md b/packages/patternfly-4/react-inline-edit-extension/README.md
new file mode 100644
index 00000000000..ced7cde7df6
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/README.md
@@ -0,0 +1,5 @@
+# @patternfly/react-inline-edit-extension
+
+Inline Edit extension provides inline editing support for PatternFly 4 React table.
+
+This package is currently an extension. Extension components do not undergo the same rigorous design or coding review process as core PatternFly components. If enough members of the community find them useful, we will work to move them into our core PatternFly system by starting the design process for the idea.
diff --git a/packages/patternfly-4/react-inline-edit-extension/build/copyStyles.js b/packages/patternfly-4/react-inline-edit-extension/build/copyStyles.js
new file mode 100644
index 00000000000..3c501ed0ad6
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/build/copyStyles.js
@@ -0,0 +1,42 @@
+/* eslint-disable no-case-declarations */
+const { copySync, readFileSync, writeFileSync } = require('fs-extra');
+const { resolve, dirname, join } = require('path');
+const { parse: parseCSS, stringify: stringifyCSS } = require('css');
+
+const baseCSSFilename = 'patternfly-base.css';
+const stylesDir = resolve(__dirname, '../dist/styles');
+const pfDir = dirname(require.resolve(`@patternfly/patternfly/${baseCSSFilename}`));
+
+const css = readFileSync(join(pfDir, baseCSSFilename), 'utf8');
+const ast = parseCSS(css);
+
+const unusedSelectorRegEx = /(\.fas?|\.sr-only)/;
+const unusedKeyFramesRegEx = /fa-/;
+const unusedFontFamilyRegEx = /Font Awesome 5 Free/;
+const ununsedFontFilesRegExt = /(fa-|\.html$|\.css$)/;
+
+// Core provides font awesome fonts and utlities. React does not use these
+ast.stylesheet.rules = ast.stylesheet.rules.filter(rule => {
+ switch (rule.type) {
+ case 'rule':
+ return !rule.selectors.some(sel => unusedSelectorRegEx.test(sel));
+ case 'keyframes':
+ return !unusedKeyFramesRegEx.test(rule.name);
+ case 'charset':
+ case 'comment':
+ return false;
+ case 'font-face':
+ const fontFamilyDecl = rule.declarations.find(decl => decl.property === 'font-family');
+ return !unusedFontFamilyRegEx.test(fontFamilyDecl.value);
+ default:
+ return true;
+ }
+});
+
+copySync(join(pfDir, 'assets/images'), join(stylesDir, 'assets/images'));
+copySync(join(pfDir, 'assets/fonts'), join(stylesDir, 'assets/fonts'), {
+ filter(src) {
+ return !ununsedFontFilesRegExt.test(src);
+ }
+});
+writeFileSync(join(stylesDir, 'base.css'), stringifyCSS(ast));
diff --git a/packages/patternfly-4/react-inline-edit-extension/build/snapshot-serializer.js b/packages/patternfly-4/react-inline-edit-extension/build/snapshot-serializer.js
new file mode 100644
index 00000000000..0edb3cb5b4c
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/build/snapshot-serializer.js
@@ -0,0 +1,8 @@
+const fs = require('fs');
+const { createSerializer } = require('@patternfly/react-styles/snapshot-serializer');
+
+const pf4CSS = fs.readFileSync(require.resolve('@patternfly/patternfly/patternfly-base.css'), 'utf8');
+
+module.exports = createSerializer({
+ globalCSS: pf4CSS.match(/:root\W?\{(.|\n)*?\}/)[0]
+});
diff --git a/packages/patternfly-4/react-inline-edit-extension/package.json b/packages/patternfly-4/react-inline-edit-extension/package.json
new file mode 100644
index 00000000000..282446e3970
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "@patternfly/react-inline-edit-extension",
+ "version": "1.0.0",
+ "description": "This library provides inline editing support for PatternFly 4 React table",
+ "main": "dist/js/index.js",
+ "module": "dist/esm/index.js",
+ "types": "dist/js/index.d.ts",
+ "sideEffects": false,
+ "publishConfig": {
+ "access": "public",
+ "tag": "prerelease"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/patternfly/patternfly-react.git"
+ },
+ "keywords": [
+ "react",
+ "patternfly",
+ "table",
+ "reacttabular"
+ ],
+ "author": "Red Hat",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/patternfly/patternfly-react/issues"
+ },
+ "homepage": "https://github.com/patternfly/patternfly-react/tree/master/packages/patternfly-4/",
+ "dependencies": {
+ "@patternfly/patternfly": "1.0.244",
+ "@patternfly/react-core": "^2.9.2",
+ "@patternfly/react-table": "^1.3.4",
+ "@patternfly/react-icons": "^3.6.1",
+ "@patternfly/react-styles": "^2.4.0",
+ "classnames": "^2.2.5",
+ "exenv": "^1.2.2",
+ "reactabular-table": "^8.14.0"
+ },
+ "peerDependencies": {
+ "prop-types": "^15.6.1",
+ "react": "^16.4.0",
+ "react-dom": "^15.6.2 || ^16.4.0"
+ },
+ "scripts": {
+ "build": "yarn build:babel && yarn build:ts",
+ "build:babel": "concurrently \"yarn build:babel:cjs\" \"yarn build:babel:esm\"",
+ "build:babel:cjs": "cross-env BABEL_ENV=production:cjs babel src --out-dir dist/js",
+ "build:babel:esm": "cross-env BABEL_ENV=production:esm babel src --out-dir dist/esm",
+ "build:ts": "node ./scripts/copyTS.js",
+ "postbuild": "node ./build/copyStyles.js"
+ },
+ "optionalDependencies": {
+ "@patternfly/react-tokens": "^2.0.2"
+ },
+ "devDependencies": {
+ "css": "^2.2.3",
+ "fs-extra": "^6.0.1",
+ "glob": "^7.1.2",
+ "npmlog": "^4.1.2"
+ }
+}
diff --git a/packages/patternfly-4/react-inline-edit-extension/scripts/copyTS.js b/packages/patternfly-4/react-inline-edit-extension/scripts/copyTS.js
new file mode 100644
index 00000000000..7f55c3a234a
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/scripts/copyTS.js
@@ -0,0 +1,17 @@
+const path = require('path');
+const glob = require('glob');
+const fse = require('fs-extra');
+const log = require('npmlog');
+
+const srcDir = path.join('./src');
+const distDir = path.join('./dist/js');
+
+const files = glob.sync('**/*.d.ts', {
+ cwd: srcDir
+});
+files.forEach(file => {
+ const from = path.join(srcDir, file);
+ const to = path.join(distDir, file);
+ log.info('copyTS', `${from} --> ${to}`);
+ fse.copySync(from, to);
+});
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/CancelButton/CancelButton.js b/packages/patternfly-4/react-inline-edit-extension/src/components/CancelButton/CancelButton.js
new file mode 100644
index 00000000000..8d0ea9c869d
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/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-inline-edit-extension/src/components/CancelButton/CancelButton.test.js b/packages/patternfly-4/react-inline-edit-extension/src/components/CancelButton/CancelButton.test.js
new file mode 100644
index 00000000000..93336044229
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/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-inline-edit-extension/src/components/CancelButton/__snapshots__/CancelButton.test.js.snap b/packages/patternfly-4/react-inline-edit-extension/src/components/CancelButton/__snapshots__/CancelButton.test.js.snap
new file mode 100644
index 00000000000..95184fc1bc9
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/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-inline-edit-extension/src/components/CancelButton/index.js b/packages/patternfly-4/react-inline-edit-extension/src/components/CancelButton/index.js
new file mode 100644
index 00000000000..c4dc29a593a
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/CancelButton/index.js
@@ -0,0 +1 @@
+export { default as CancelButton } from './CancelButton';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/ConfirmButton/ConfirmButton.js b/packages/patternfly-4/react-inline-edit-extension/src/components/ConfirmButton/ConfirmButton.js
new file mode 100644
index 00000000000..25be8e0353f
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/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-inline-edit-extension/src/components/ConfirmButton/ConfirmButton.test.js b/packages/patternfly-4/react-inline-edit-extension/src/components/ConfirmButton/ConfirmButton.test.js
new file mode 100644
index 00000000000..2cd4b86d46e
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/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-inline-edit-extension/src/components/ConfirmButton/__snapshots__/ConfirmButton.test.js.snap b/packages/patternfly-4/react-inline-edit-extension/src/components/ConfirmButton/__snapshots__/ConfirmButton.test.js.snap
new file mode 100644
index 00000000000..c8d70e4c57a
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/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-inline-edit-extension/src/components/ConfirmButton/index.js b/packages/patternfly-4/react-inline-edit-extension/src/components/ConfirmButton/index.js
new file mode 100644
index 00000000000..4ccccc70087
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/ConfirmButton/index.js
@@ -0,0 +1 @@
+export { default as ConfirmButton } from './ConfirmButton';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/ConfirmButtons.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/ConfirmButtons.js
new file mode 100644
index 00000000000..2192904037c
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/ConfirmButtons.js
@@ -0,0 +1,95 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { css } from '@patternfly/react-styles';
+import { CancelButton } from '../CancelButton';
+import { ConfirmButton } from '../ConfirmButton';
+import { inlineEditCss, inlineEditStyles as styles } from './css/inline-edit-css';
+
+inlineEditCss.inject();
+
+const buttonsTopPosition = (window, rowDimensions, bold) => {
+ const boldShift = bold ? -1 : 0;
+ return {
+ bottom: window.height - rowDimensions.top - 1 + boldShift,
+ right: window.width - rowDimensions.right + 10
+ };
+};
+
+const buttonsBottomPosition = (window, rowDimensions, bold) => {
+ const boldShift = bold ? -1 : 0;
+ return {
+ top: rowDimensions.bottom - 1 + boldShift,
+ right: window.width - rowDimensions.right + 10
+ };
+};
+
+const ConfirmButtons = ({
+ messages: { confirmButtonLabel, cancelButtonLabel },
+ onConfirm,
+ onCancel,
+ environment,
+ buttonsOnTop,
+ boldBorder
+}) => {
+ if (environment == null) {
+ return null;
+ }
+ const { window, row } = environment;
+
+ const positionStyle = buttonsOnTop
+ ? buttonsTopPosition(window, row, boldBorder)
+ : buttonsBottomPosition(window, row, boldBorder);
+
+ const className = css(
+ styles.tableInlineEditButtons,
+ buttonsOnTop ? styles.modifiers.top : styles.modifiers.bottom,
+ boldBorder && styles.modifiers.bold
+ );
+
+ return (
+
+
+
+
+ );
+};
+
+ConfirmButtons.defaultProps = {
+ onConfirm: () => undefined,
+ onCancel: () => undefined,
+ buttonsOnTop: false,
+ boldBorder: false,
+ environment: undefined,
+ messages: {
+ confirmButtonLabel: 'Save',
+ cancelButtonLabel: 'Cancel'
+ }
+};
+
+ConfirmButtons.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,
+ boldBorder: PropTypes.bool,
+ messages: PropTypes.shape({
+ confirmButtonLabel: PropTypes.string,
+ cancelButtonLabel: PropTypes.string
+ })
+};
+
+export default ConfirmButtons;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/ConfirmButtons.test.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/ConfirmButtons.test.js
new file mode 100644
index 00000000000..ac12f4a72d7
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/ConfirmButtons.test.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import { mount } from 'enzyme/build';
+
+import { default as ConfirmButtons } from './ConfirmButtons';
+
+const getConfirmButtons = () => (
+
+);
+
+describe('ConfirmButtons', () => {
+ test('renders correctly', () => {
+ const view = mount(getConfirmButtons());
+ expect(view).toMatchSnapshot();
+ });
+});
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/InlineEdit.api.test.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/InlineEdit.api.test.js
new file mode 100644
index 00000000000..eaa3ad8ca31
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/InlineEdit.api.test.js
@@ -0,0 +1,170 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { Table, TableHeader, TableBody, RowWrapper } from '@patternfly/react-table';
+import { TextInput } from '@patternfly/react-core';
+
+import { default as editableRowWrapper } from './editableRowWrapper';
+import { default as editableTableBody } from './editableTableBody';
+import { inlineEditFormatterFactory } from './utils';
+import { TableEditConfirmation } from './constants';
+import { TableTextInput } from '../TableTextInput';
+
+import { rows, columns } from '../../test-helpers/data-sets';
+import { mockClosest, makeTableId } from '../../test-helpers/helpers';
+
+const firstColTitle = 'editcolfirst';
+const lastColTitle = 'editcollast';
+const firstInputName = 'inputOne';
+const secondInputName = 'inputTwo';
+
+const editRowIndex = 2;
+
+const firstColEditedRowInputId = {
+ rowIndex: editRowIndex,
+ columnIndex: 0,
+ column: {
+ property: firstColTitle
+ },
+ name: firstInputName
+};
+
+const lastColEditedRowInputId = {
+ rowIndex: editRowIndex,
+ columnIndex: columns.length + 2 - 1,
+ column: {
+ property: lastColTitle
+ },
+ name: secondInputName
+};
+
+describe('Editable table', () => {
+ let mountOptions;
+ let container;
+
+ beforeEach(() => {
+ container = mount();
+
+ mountOptions = {
+ attachTo: container.getDOMNode()
+ };
+
+ // mock closest for selecting the first column (firing onEditCellClicked) and resolving table for confirm buttons
+ mockClosest(
+ {
+ '[data-key]': {
+ getAttribute: () => firstColEditedRowInputId.columnIndex,
+ contains: elem => elem.getAttribute('id') === makeTableId(firstColEditedRowInputId)
+ },
+ '[id]': {
+ getAttribute: () => makeTableId(firstColEditedRowInputId)
+ }
+ },
+ selector => (selector === 'table' ? container.getDOMNode().getElementsByTagName('table')[0] : undefined),
+ true
+ );
+ });
+
+ afterEach(() => {
+ container.unmount();
+ });
+
+ test('should call correct function', () => {
+ const ComposedBody = editableTableBody(TableBody);
+ const ComposedRowWrapper = editableRowWrapper(RowWrapper);
+
+ const onBlur = jest.fn();
+
+ const inlineEditingFormatter = inlineEditFormatterFactory({
+ renderEdit: (value, { columnIndex, rowIndex, column }, { activeEditId }) => {
+ const firstInputId = makeTableId({
+ rowIndex,
+ columnIndex,
+ column,
+ name: firstInputName
+ });
+ const secondInputId = makeTableId({
+ rowIndex,
+ columnIndex,
+ column,
+ name: secondInputName
+ });
+ return (
+
+
+
+
+ );
+ }
+ });
+
+ const editableCols = [
+ {
+ title: firstColTitle,
+ cellFormatters: [inlineEditingFormatter]
+ },
+ ...columns,
+ {
+ title: lastColTitle,
+ cellFormatters: [inlineEditingFormatter]
+ }
+ ];
+
+ const editableRows = rows.map(row => ({
+ ...row,
+ cells: ['', ...row.cells, ''] // add two new columns
+ }));
+ editableRows[editRowIndex].isEditing = true;
+
+ const editConfig = {
+ activeEditId: makeTableId(lastColEditedRowInputId),
+ onEditCellClicked: jest.fn(),
+ editConfirmationType: TableEditConfirmation.ROW,
+ onEditConfirmed: jest.fn(),
+ onEditCanceled: jest.fn()
+ };
+ const view = mount(
+ ,
+ mountOptions
+ );
+
+ // calls onBlur properly
+ const editTextInputWrapper = view
+ .find(TextInput)
+ .find(`#${makeTableId(lastColEditedRowInputId)}`)
+ .first();
+ editTextInputWrapper.prop('onBlur')({ currentTarget: { value: 'water' } });
+
+ expect(onBlur).toHaveBeenCalled();
+ expect(onBlur.mock.calls).toHaveLength(1);
+ expect(onBlur.mock.calls[0][0]).toBe('water');
+
+ // responds to cell click
+ view
+ .find(`#${makeTableId(firstColEditedRowInputId)}`)
+ .hostNodes()
+ .simulate('mousedown');
+
+ // should immediately call onEditCellClicked
+ setTimeout(() => expect(editConfig.onEditCellClicked).toHaveBeenCalled(), 0);
+
+ // responds to confirmation button clicks
+ view.find('.pf-c-table__inline-edit-buttons button.pf-c-button.pf-m-primary').simulate('mouseup');
+ expect(editConfig.onEditConfirmed).toHaveBeenCalled();
+
+ view.find('.pf-c-table__inline-edit-buttons button.pf-c-button.pf-m-plain').simulate('mouseup');
+ expect(editConfig.onEditCanceled).toHaveBeenCalled();
+ });
+});
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/InlineEdit.docs.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/InlineEdit.docs.js
new file mode 100644
index 00000000000..5700703aa6d
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/InlineEdit.docs.js
@@ -0,0 +1,13 @@
+import EditableTable from './examples/EditableTable';
+import EditableTableColumn from './examples/EditableTableColumn';
+import CollapsibleEditableTable from './examples/CollapsibleEditableTable';
+
+export default {
+ title: 'Inline Edit Table',
+ components: {},
+ examples: [
+ { component: EditableTable, title: 'Editable table With Inline Edit Row' },
+ { component: EditableTableColumn, title: 'Editable Table With Inline Edit Columns' },
+ { component: CollapsibleEditableTable, title: 'Editable Table With Collapsible Rows' }
+ ]
+};
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/__snapshots__/ConfirmButtons.test.js.snap b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/__snapshots__/ConfirmButtons.test.js.snap
new file mode 100644
index 00000000000..beb6f13405a
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/__snapshots__/ConfirmButtons.test.js.snap
@@ -0,0 +1,211 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ConfirmButtons renders correctly 1`] = `
+.pf-c-button.pf-m-primary {
+ display: inline-block;
+ position: relative;
+ padding: 0.375rem 1rem 0.375rem 1rem;
+ font-size: 1rem;
+ font-weight: 500;
+ line-height: 1.5;
+ text-align: center;
+ white-space: nowrap;
+ user-select: none;
+ border: 0px;
+ border-radius: 3px;
+ color: #ffffff;
+ background-color: #007bba;
+}
+.pf-c-button.pf-m-plain {
+ display: inline-block;
+ position: relative;
+ padding: 0.375rem 1rem 0.375rem 1rem;
+ font-size: 1rem;
+ font-weight: 500;
+ line-height: 1.5;
+ text-align: center;
+ white-space: nowrap;
+ user-select: none;
+ border: 0px;
+ border-radius: 3px;
+ color: #72767b;
+}
+.pf-c-table__inline-edit-buttons.pf-m-top.pf-m-bold {
+ display: block;
+ position: fixed;
+ z-index: 1000;
+ padding: 4px;
+ margin: 0px;
+ background: #def3ff;
+ border: 1px solid #7dc3e8;
+ border-bottom: 0;
+ border-width: 3px;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/__snapshots__/editableRowWrapper.test.js.snap b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/__snapshots__/editableRowWrapper.test.js.snap
new file mode 100644
index 00000000000..541f9fee115
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/__snapshots__/editableRowWrapper.test.js.snap
@@ -0,0 +1,67 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`editableRowWrapper renders correctly 1`] = `
+.pf-c-table__editable-row {
+ display: block;
+}
+
+,
+ ],
+ Array [
+ null,
+ ],
+ Array [
+
,
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+>
+
+
+
+
+`;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/constants.d.ts b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/constants.d.ts
new file mode 100644
index 00000000000..0dd282396c7
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/constants.d.ts
@@ -0,0 +1,7 @@
+export const TableEditConfirmation: {
+ NONE: 'NONE',
+ ROW: 'ROW',
+ NO_CONFIRM_ROW: 'NO_CONFIRM_ROW',
+ TABLE_TOP: 'TABLE_TOP',
+ TABLE_BOTTOM: 'TABLE_BOTTOM'
+};
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/constants.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/constants.js
new file mode 100644
index 00000000000..44d37c295f1
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/constants.js
@@ -0,0 +1,7 @@
+export const TableEditConfirmation = {
+ NONE: 'NONE',
+ ROW: 'ROW',
+ NO_CONFIRM_ROW: 'NO_CONFIRM_ROW',
+ TABLE_TOP: 'TABLE_TOP',
+ TABLE_BOTTOM: 'TABLE_BOTTOM'
+};
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/css/inline-edit-css.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/css/inline-edit-css.js
new file mode 100644
index 00000000000..fcc7ed75172
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/css/inline-edit-css.js
@@ -0,0 +1,86 @@
+import { StyleSheet } from '@patternfly/react-styles';
+
+const pfColorBlue50 = '#def3ff';
+const pfColorBlue200 = '#7dc3e8';
+
+export const inlineEditCss = StyleSheet.parse(`
+ tr.pf-c-table__editable-row {
+ &:hover,
+ &.pf-m-editing {
+ background: ${pfColorBlue50} !important;
+ td {
+ border-bottom: 1px solid ${pfColorBlue200} !important;
+ border-top: 1px solid ${pfColorBlue200} !important;
+
+ &:first-child {
+ border-left: 1px solid ${pfColorBlue200} !important;
+ }
+
+ &:last-child {
+ border-right: 1px solid ${pfColorBlue200} !important;
+ }
+ }
+ }
+
+ &.pf-m-table-editing-first-row {
+ border-top: 3px solid ${pfColorBlue200} !important;
+ }
+
+ &.pf-m-table-editing-last-row {
+ border-bottom: 3px solid ${pfColorBlue200} !important;
+ }
+
+ input {
+ display: block;
+ background: var(--pf-global--BackgroundColor--100);
+ border: 1px solid var(--pf-global--BorderColor);
+
+ &:hover {
+ cursor: text;
+ }
+ }
+ }
+
+ .pf-c-table__inline-edit-buttons {
+ position: fixed;
+ z-index: 1000;
+ padding: 4px;
+ margin: 0;
+ background: ${pfColorBlue50};
+ border: 1px solid ${pfColorBlue200};
+
+ &.pf-m-top {
+ border-bottom: 0;
+ }
+
+ &.pf-m-bottom {
+ border-top: 0;
+ }
+
+ &.pf-m-bold {
+ border-width: 3px;
+ }
+
+ button {
+ margin-left: 4px;
+
+ &:first-child {
+ margin-left: 0;
+ }
+ }
+ }
+
+`);
+
+export const inlineEditStyles = {
+ tableEditableRow: 'pf-c-table__editable-row',
+ tableInlineEditButtons: 'pf-c-table__inline-edit-buttons',
+ modifiers: {
+ tableEditingFirstRow: 'pf-m-table-editing-first-row ',
+ tableEditingLastRow: 'pf-m-table-editing-last-row',
+ editing: 'pf-m-editing',
+ top: 'pf-m-top',
+ bottom: 'pf-m-bottom',
+ bold: 'pf-m-bold'
+ }
+};
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableRowWrapper.d.ts b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableRowWrapper.d.ts
new file mode 100644
index 00000000000..742af07ad4e
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableRowWrapper.d.ts
@@ -0,0 +1,22 @@
+import { Component, FunctionComponent } from 'react';
+import { RowWrapper, RowWrapperRow } from '@patternfly/react-table';
+
+import { EditConfig } from './editableTableBody'
+
+export interface EditableRowWrapperRow extends RowWrapperRow {
+ isEditing: boolean;
+ isTableEditing: boolean;
+ isFirstVisible: boolean;
+ isLastVisible: boolean;
+ editConfig: EditConfig;
+}
+
+export interface EditableRowWrapperProps extends RowWrapper {
+ row: EditableRowWrapperRow;
+}
+
+export interface EditableRowWrapper {
+ (bodyComponent: Component): FunctionComponent;
+}
+
+export default EditableRowWrapper;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableRowWrapper.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableRowWrapper.js
new file mode 100644
index 00000000000..490b0af94ad
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableRowWrapper.js
@@ -0,0 +1,238 @@
+import React from 'react';
+import { createPortal } from 'react-dom';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { css } from '@patternfly/react-styles';
+import { RowWrapper as ReactTableRowWrapper } from '@patternfly/react-table';
+import {
+ combineFunctions,
+ shallowLeftSideEquals,
+ getBoundingClientRect,
+ getClientWindowDimensions
+} from './utils/utils';
+import ConfirmButtons from './ConfirmButtons';
+import { TableEditConfirmation } from './constants';
+import { inlineEditCss, inlineEditStyles as styles } from './css/inline-edit-css';
+
+inlineEditCss.inject();
+
+const propTypes = {
+ ...ReactTableRowWrapper.propTypes,
+ trRef: PropTypes.func,
+ className: PropTypes.string,
+ onScroll: PropTypes.func,
+ onResize: PropTypes.func,
+ row: PropTypes.shape({
+ isOpen: PropTypes.bool,
+ isExpanded: PropTypes.bool,
+ isEditing: PropTypes.bool,
+ isTableEditing: PropTypes.bool,
+ isFirstVisible: PropTypes.bool,
+ isLastVisible: PropTypes.bool,
+ isChildEditing: PropTypes.bool,
+ isParentEditing: PropTypes.bool,
+ isLastVisibleParent: PropTypes.bool,
+ editConfig: PropTypes.object
+ }),
+ rowProps: PropTypes.object
+};
+
+const defaultProps = {
+ ...ReactTableRowWrapper.defaultProps,
+ trRef: undefined,
+ className: '',
+ onScroll: undefined,
+ onResize: undefined,
+ row: {
+ isOpen: undefined,
+ isExpanded: undefined,
+ isEditing: undefined,
+ isTableEditing: undefined,
+ isFirstVisible: undefined,
+ isLastVisible: undefined,
+ isChildEditing: undefined,
+ isParentEditing: undefined,
+ isLastVisibleParent: undefined,
+ editConfig: undefined
+ },
+ rowProps: null
+};
+
+// TableEditConfirmation constants like TABLE_TOP cannot be referenced but must be hardcoded due to this issue:
+// https://github.com/reactjs/react-docgen/issues/317#issue-393678795
+const tableConfirmationMapper = {
+ TABLE_TOP: {
+ hasConfirmationButtons: ({ isTableEditing, isFirstVisible }) => isTableEditing && isFirstVisible,
+ isTableConfirmation: () => true,
+ areButtonsOnTop: () => true,
+ hasBoldBorder: () => true,
+ getEditStyles: ({ isTableEditing, isFirstVisible }) =>
+ css(styles.tableEditableRow, isTableEditing && isFirstVisible && styles.modifiers.tableEditingFirstRow)
+ },
+ TABLE_BOTTOM: {
+ hasConfirmationButtons: ({ isTableEditing, isLastVisible }) => isTableEditing && isLastVisible,
+ isTableConfirmation: () => true,
+ areButtonsOnTop: () => false,
+ hasBoldBorder: () => true,
+ getEditStyles: ({ isTableEditing, isLastVisible }) =>
+ css(styles.tableEditableRow, isTableEditing && isLastVisible && styles.modifiers.tableEditingLastRow)
+ },
+ ROW: {
+ hasConfirmationButtons: ({ isEditing, isParentEditing, isLastVisibleParent, isChildEditing, isLastVisible }) =>
+ isEditing &&
+ !(isChildEditing && isParentEditing) && // buttons can't appear in the middle
+ !(isParentEditing && isLastVisible) && // parent will show the buttons on top
+ !(isChildEditing && !isLastVisibleParent), // child will show the buttons on bottom
+ isTableConfirmation: () => false,
+ areButtonsOnTop: ({ isLastVisible, isLastVisibleParent }) => isLastVisible || isLastVisibleParent,
+ hasBoldBorder: () => false,
+ getEditStyles: ({ isEditing }) => css(styles.tableEditableRow, isEditing && styles.modifiers.editing)
+ },
+ NO_CONFIRM_ROW: {
+ hasConfirmationButtons: () => false,
+ isTableConfirmation: () => false,
+ areButtonsOnTop: () => false,
+ hasBoldBorder: () => false,
+ getEditStyles: ({ isEditing }) => css(styles.tableEditableRow, isEditing && styles.modifiers.editing)
+ },
+ NONE: {
+ hasConfirmationButtons: () => false,
+ isTableConfirmation: () => false,
+ areButtonsOnTop: () => false,
+ hasBoldBorder: () => false,
+ getEditStyles: () => css(styles.tableEditableRow)
+ }
+};
+
+const getTableConfirmation = ({ editConfig }) =>
+ tableConfirmationMapper[editConfig && editConfig.editConfirmationType] ||
+ tableConfirmationMapper[TableEditConfirmation.NONE];
+
+const editableRowWrapper = RowWrapperComponent => {
+ class RowWrapper extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ rowDimensions: {},
+ window: {},
+ ...RowWrapper.getDerivedStateFromProps(props)
+ };
+ }
+
+ static getDerivedStateFromProps = (props, state) => ({
+ hasConfirmationButtons: getTableConfirmation(props.row).hasConfirmationButtons(props.row)
+ });
+
+ setStateWith2dEquals = newState => {
+ this.setState(oldState =>
+ Object.keys(newState).find(key => !shallowLeftSideEquals(newState[key], oldState[key])) ? newState : null
+ );
+ };
+
+ componentDidMount() {
+ if (this.state.hasConfirmationButtons) {
+ this.fetchClientDimensions();
+ }
+ }
+
+ saveRowDimensions = element => {
+ if (element) {
+ this.element = element;
+ this.tableElem = element.closest('table');
+ }
+ this.updateRowDimensions();
+ };
+
+ updateRowDimensions = () => {
+ if (this.element) {
+ this.setStateWith2dEquals({
+ rowDimensions: getBoundingClientRect(this.element)
+ });
+ }
+ };
+
+ handleScroll = event => {
+ this.updateRowDimensions();
+ };
+
+ handleResize = event => {
+ this.fetchClientDimensions();
+ this.updateRowDimensions();
+ };
+
+ fetchClientDimensions() {
+ this.setStateWith2dEquals({
+ window: getClientWindowDimensions()
+ });
+ }
+
+ getConfirmationButtons() {
+ const { row, rowProps, ...props } = this.props;
+ const { isLastVisible, isParentEditing, isLastVisibleParent, editConfig } = row;
+
+ if (!editConfig) {
+ return null;
+ }
+ const { onEditConfirmed, onEditCanceled } = editConfig;
+ const tableConfirmation = getTableConfirmation(row);
+
+ let confirmButtons;
+ if (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({ isLastVisible, isParentEditing, isLastVisibleParent })}
+ boldBorder={tableConfirmation.hasBoldBorder()}
+ environment={{
+ window: this.state.window,
+ row: getBoundingClientRect(this.element)
+ }}
+ />,
+ this.tableElem ? this.tableElem.parentNode : document.body
+ );
+ }
+ return confirmButtons;
+ }
+
+ render() {
+ const {
+ trRef,
+ className,
+ onScroll,
+ onResize,
+ row: { isFirstVisible, isLastVisible, isEditing, isTableEditing, editConfig }
+ } = this.props;
+ const { hasConfirmationButtons } = this.state;
+ const trClassName = getTableConfirmation({ editConfig }).getEditStyles({
+ isEditing,
+ isTableEditing,
+ isFirstVisible,
+ isLastVisible
+ });
+
+ return (
+
+
+ {hasConfirmationButtons && this.getConfirmationButtons()}
+
+ );
+ }
+ }
+
+ RowWrapper.propTypes = propTypes;
+ RowWrapper.defaultProps = defaultProps;
+
+ return RowWrapper;
+};
+
+export default editableRowWrapper;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableRowWrapper.test.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableRowWrapper.test.js
new file mode 100644
index 00000000000..0024bec2777
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableRowWrapper.test.js
@@ -0,0 +1,125 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { mount } from 'enzyme/build';
+
+import { default as editableRowWrapper } from './editableRowWrapper';
+import { TableEditConfirmation } from './constants';
+import { mockClosest } from '../../test-helpers/helpers';
+
+const TestRow = ({ trRef }) =>
;
+
+TestRow.propTypes = {
+ trRef: PropTypes.func
+};
+
+TestRow.defaultProps = {
+ trRef: null
+};
+
+const getRowWrapper = (row, props) => {
+ const RowWrapper = editableRowWrapper(TestRow);
+ return ;
+};
+
+const buildRow = (row = {}, editConfirmationType = TableEditConfirmation.ROW) => ({
+ ...row,
+ editConfig: { editConfirmationType }
+});
+
+describe('editableRowWrapper', () => {
+ let mountOptions;
+ let container;
+
+ beforeEach(() => {
+ container = mount(
+
+ );
+ mountOptions = {
+ attachTo: container.find('tbody').getDOMNode()
+ };
+
+ mockClosest(null, selector => (selector === 'table' ? container.getDOMNode() : undefined), true);
+ });
+
+ afterEach(() => {
+ container.unmount();
+ });
+
+ test('renders correctly', () => {
+ const trRef = jest.fn();
+ const view = mount(
+ getRowWrapper(buildRow({}, null), {
+ onResize: jest.fn(),
+ onScroll: jest.fn(),
+ trRef
+ }),
+ mountOptions
+ );
+ expect(view).toMatchSnapshot();
+ expect(trRef).toHaveBeenCalled();
+ });
+
+ test('sets editable row classname', () => {
+ [...Object.keys(TableEditConfirmation), null].forEach(confirmationType => {
+ const view = mount(getRowWrapper(buildRow({}, confirmationType)), mountOptions);
+ expect(view.find('.pf-c-table__editable-row')).toHaveLength(1);
+ view.detach();
+ });
+ });
+
+ test('has inline edit buttons', () => {
+ [
+ buildRow({ isEditing: true }),
+ buildRow({ isEditing: true, isParentEditing: true }), // expandable
+ buildRow({ isEditing: true, isChildEditing: true, isLastVisibleParent: true }), // expandable
+ buildRow({ isTableEditing: true, isFirstVisible: true }, TableEditConfirmation.TABLE_TOP),
+ buildRow({ isTableEditing: true, isLastVisible: true }, TableEditConfirmation.TABLE_BOTTOM)
+ ].forEach(row => {
+ const view = mount(getRowWrapper(row), mountOptions);
+ expect(view.find('.pf-c-table__inline-edit-buttons')).toHaveLength(1);
+ view.detach();
+ });
+ });
+
+ test('does not have inline edit buttons', () => {
+ [
+ buildRow({ isEditing: true }, TableEditConfirmation.NONE),
+ buildRow({ isEditing: true }, TableEditConfirmation.TABLE_TOP),
+ buildRow({ isEditing: true }, TableEditConfirmation.TABLE_BOTTOM),
+ buildRow({ isEditing: true, isParentEditing: true, isLastVisible: true }), // expandable
+ buildRow({ isEditing: true, isChildEditing: true }), // expandable
+ buildRow({ isTableEditing: true, isLastVisible: true }, TableEditConfirmation.NONE),
+ buildRow({ isTableEditing: true, isLastVisible: true }),
+ buildRow({ isTableEditing: true }, TableEditConfirmation.TABLE_TOP),
+ buildRow({ isTableEditing: true }, TableEditConfirmation.TABLE_BOTTOM),
+ buildRow({ isTableEditing: true, isLastVisible: true }, TableEditConfirmation.TABLE_TOP),
+ buildRow({ isTableEditing: true, isFirstVisible: true }, TableEditConfirmation.TABLE_BOTTOM)
+ ].forEach(row => {
+ const view = mount(getRowWrapper(row), mountOptions);
+ expect(view.find('.pf-c-table__inline-edit-buttons')).toHaveLength(0);
+ view.detach();
+ });
+ });
+
+ test('onEditConfirmed and onEditCanceled called', () => {
+ const onEditConfirmed = jest.fn();
+ const onEditCanceled = jest.fn();
+ const row = {
+ isEditing: true,
+ editConfig: {
+ editConfirmationType: TableEditConfirmation.ROW,
+ onEditConfirmed,
+ onEditCanceled
+ }
+ };
+
+ const view = mount(getRowWrapper(row, null, React.Fragment), mountOptions);
+ view.find('.pf-c-table__inline-edit-buttons button.pf-c-button.pf-m-primary').simulate('mouseup');
+ expect(onEditConfirmed).toHaveBeenCalled();
+
+ view.find('.pf-c-table__inline-edit-buttons button.pf-c-button.pf-m-plain').simulate('mouseup');
+ expect(onEditCanceled).toHaveBeenCalled();
+ });
+});
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableTableBody.d.ts b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableTableBody.d.ts
new file mode 100644
index 00000000000..1e18ead8296
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableTableBody.d.ts
@@ -0,0 +1,27 @@
+import { Component, FunctionComponent, MouseEvent } from 'react';
+import { TableBodyProps, IRowData, IExtraRowData } from '@patternfly/react-table';
+import { OneOf } from '@patternfly/react-core';
+
+import { TableEditConfirmation } from './constants'
+
+export interface IEditedCellData extends IExtraRowData {
+ columnIndex: number;
+ elementId?: string;
+}
+
+export interface EditConfig {
+ editConfirmationType?: OneOf;
+ onEditCellClicked?(value: MouseEvent, row: IRowData, extra: IEditedCellData): void;
+ onEditConfirmed?(value: MouseEvent, row: IRowData, rowProps: IExtraRowData): void;
+ onEditCanceled?(value: MouseEvent, row: IRowData, rowProps: IExtraRowData): void;
+}
+
+export interface InlineEditBodyProps extends TableBodyProps {
+ editConfig: EditConfig;
+}
+
+export interface EditableTableBody {
+ (bodyComponent: Component): FunctionComponent;
+}
+
+export default EditableTableBody;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableTableBody.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableTableBody.js
new file mode 100644
index 00000000000..29978e1f727
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/editableTableBody.js
@@ -0,0 +1,142 @@
+import React from 'react';
+import { TableContext, TableBody, isRowExpanded } from '@patternfly/react-table';
+
+import PropTypes from 'prop-types';
+import { TableEditConfirmation } from './constants';
+import { showIdWarnings } from './utils/utils';
+
+const propTypes = {
+ ...TableBody.propTypes,
+ editConfig: PropTypes.shape({
+ editConfirmationType: PropTypes.oneOf(Object.values(TableEditConfirmation)),
+ onEditCellClicked: PropTypes.func,
+ onEditConfirmed: PropTypes.func,
+ onEditCanceled: PropTypes.func
+ }).isRequired,
+ /** Function that is fired when user clicks on a row if not editing. */
+ onRowClick: PropTypes.func
+};
+
+const defaultProps = {
+ ...TableBody.defaultProps,
+ editConfig: null,
+ onRowClick: () => undefined
+};
+
+const resolveCascadeEditability = rows => {
+ const isRowExpandedIndexes = new Set(
+ rows.map((row, idx) => (isRowExpanded(row, rows) ? idx : null)).filter(row => row !== null)
+ );
+
+ // flag parents and their children which are edited together
+ rows
+ .filter(
+ (row, idx) =>
+ row.parent !== undefined &&
+ row.isEditing &&
+ isRowExpandedIndexes.has(idx) &&
+ row.isEditableTogetherWithParent &&
+ rows[row.parent].isEditing
+ )
+ .forEach(row => {
+ rows[row.parent].isChildEditing = true;
+ row.isParentEditing = true;
+ });
+
+ const lastVisibleRow = rows.filter((row, idx) => !row.parent || isRowExpandedIndexes.has(idx)).pop();
+
+ // flag last parent row if there are only descendants under it
+ if (lastVisibleRow && lastVisibleRow.isParentEditing) {
+ let parentRow = lastVisibleRow;
+ while (parentRow.parent !== undefined && parentRow.isEditableTogetherWithParent) {
+ parentRow = rows[parentRow.parent];
+ }
+ parentRow.isLastVisibleParent = true;
+ }
+};
+
+const onRow = (event, row, rowProps, computedData, { onRowClick, editConfig }) => {
+ const { target } = event;
+ const cell = target.closest('[data-key]');
+ const cellNumber = parseInt(cell && cell.getAttribute('data-key'));
+ const hasCellNumber = !Number.isNaN(cellNumber);
+
+ let onEditCellClicked;
+
+ if (hasCellNumber && editConfig && typeof editConfig.onEditCellClicked === 'function') {
+ // resolve closest (e.g. for dropdowns) usable id of a clicked element inside a cell
+ const idElement = target.closest('[id]');
+ const elementId = idElement && cell.contains(idElement) ? idElement.getAttribute('id') || null : null;
+
+ if (!elementId) {
+ showIdWarnings(row, target);
+ }
+
+ onEditCellClicked = () => {
+ editConfig.onEditCellClicked(event, row, {
+ ...rowProps,
+ columnIndex: cellNumber,
+ elementId
+ });
+ };
+ }
+
+ // give priority to fire onChange/onBlur callbacks
+
+ setTimeout(() => {
+ if (!row.isEditing) {
+ onRowClick(event, row, rowProps, computedData);
+ if (onEditCellClicked) {
+ // edit cell after rerender
+ setTimeout(onEditCellClicked, 0);
+ }
+ } else if (onEditCellClicked) {
+ onEditCellClicked();
+ }
+ }, 0);
+};
+
+const Body = ({ BodyComponent, rows, editConfig, onRowClick, ...props }) => {
+ const isTableEditing = rows.some(row => row.isEditing);
+ const mappedRows = rows.map(row => ({
+ ...row,
+ editConfig,
+ isTableEditing
+ }));
+
+ resolveCascadeEditability(mappedRows);
+
+ return (
+
+ onRow(event, row, rowProps, computedData, { onRowClick, editConfig })
+ }
+ />
+ );
+};
+
+const editableTableBody = BodyComponent => {
+ const InlineEditBody = ({ editConfig, onRowClick, ...props }) => (
+
+ {({ rows, ...consumedProps }) => (
+
+ )}
+
+ );
+
+ InlineEditBody.propTypes = propTypes;
+ InlineEditBody.defaultProps = defaultProps;
+
+ return InlineEditBody;
+};
+
+export default editableTableBody;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/examples/CollapsibleEditableTable.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/examples/CollapsibleEditableTable.js
new file mode 100644
index 00000000000..efb702e82ad
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/examples/CollapsibleEditableTable.js
@@ -0,0 +1,284 @@
+import React from 'react';
+import { Table, TableHeader, TableBody, RowWrapper, ExpandableRowContent } from '@patternfly/react-table';
+import {
+ editableTableBody,
+ editableRowWrapper,
+ inlineEditFormatterFactory,
+ TableEditConfirmation,
+ TableTextInput
+} from '@patternfly/react-inline-edit-extension';
+
+class CollapsibleEditableTable extends React.Component {
+ makeId = ({ column, rowIndex, columnIndex, name }) =>
+ `${column.property}-${rowIndex}-${columnIndex}${name ? `-${name}` : ''}`;
+
+ constructor(props) {
+ super(props);
+
+ const childEditRenderer = (value, { column, rowIndex, rowData, columnIndex, activeEditId }) => (
+
+ {rowData.data.modules.map((module, idx) => {
+ const inlineStyle = {
+ marginLeft: idx && '1em',
+ display: 'inline-block',
+ width: '48%'
+ };
+
+ const id = this.makeId({ rowIndex, columnIndex, column, name: `module-${idx}` });
+
+ return (
+
+ this.onChange(newValue, {
+ rowIndex,
+ moduleIndex: idx
+ })
+ }
+ autoFocus={activeEditId === id}
+ />
+ );
+ })}
+
+ );
+
+ const parentEditRenderer = (value, { column, rowIndex, columnIndex, activeEditId }) => {
+ const id = this.makeId({ rowIndex, columnIndex, column, name: 'parent-repo' });
+ return (
+
+ this.onChange(newValue, {
+ rowIndex,
+ columnIndex
+ })
+ }
+ autoFocus={activeEditId === id}
+ />
+ );
+ };
+
+ const textInputFormatter = inlineEditFormatterFactory({
+ renderValue: (value, { rowData }) =>
+ rowData.hasOwnProperty('parent') ? (
+ {rowData.data.modules.filter(a => a).join(', ')}
+ ) : (
+ value
+ ),
+ renderEdit: (value, { column, columnIndex, rowIndex, rowData }, { activeEditId }) => {
+ const renderer = rowData.hasOwnProperty('parent') ? childEditRenderer : parentEditRenderer;
+ return renderer(value, { rowData, column, rowIndex, columnIndex, activeEditId });
+ }
+ });
+
+ this.state = {
+ columns: [
+ {
+ title: 'Repositories',
+ cellFormatters: [textInputFormatter]
+ },
+ {
+ title: 'Branches'
+ },
+ 'Pull requests',
+ {
+ title: 'Workspaces',
+ cellFormatters: [textInputFormatter]
+ },
+ {
+ title: 'Last Commit',
+ cellFormatters: [textInputFormatter]
+ }
+ ],
+ rows: [
+ {
+ cells: ['one', 'master', 7, 'Grey', 'five'],
+ isOpen: false
+ },
+ {
+ cells: [null],
+ data: {
+ modules: ['', '']
+ },
+ parent: 0,
+ isEditableTogetherWithParent: true
+ },
+ {
+ cells: ['uno', 'v2.3.0', 125, 'Orange', 'cinco'],
+ isOpen: false
+ },
+ {
+ cells: [null],
+ data: {
+ modules: ['storage', '']
+ },
+ parent: 2
+ },
+ {
+ cells: ['', 'master', 0, 'Blue', ''],
+ isOpen: true
+ },
+ {
+ cells: [null],
+ data: {
+ modules: ['security', 'network']
+ },
+ parent: 4,
+ isEditableTogetherWithParent: true
+ }
+ ],
+ editedRowsBackup: null,
+ activeEditId: null
+ };
+ }
+
+ onChange = (value, { rowIndex, columnIndex, moduleIndex }) => {
+ this.setState(({ rows }) => {
+ rows = [...rows];
+ const row = rows[rowIndex];
+ if (moduleIndex != null) {
+ row.data.modules[moduleIndex] = value;
+ } else {
+ const shiftedColumnIndex = columnIndex - 1; // to take Expandable Column into account;
+ row.cells[shiftedColumnIndex] = value;
+ }
+ return { rows, activeEditId: null };
+ });
+ };
+
+ onEditCellClicked = (event, clickedRow, { rowIndex, columnIndex, elementId }) => {
+ const EXPANDABLE_COL = 0;
+ const ACTIONS_COL = 6;
+
+ if (elementId !== this.state.activeEditId && clickedRow.isEditing && columnIndex !== ACTIONS_COL) {
+ this.setState(({ rows }) => ({
+ activeEditId: elementId,
+ rows: rows.map((row, id) => {
+ if (id === rowIndex && columnIndex === EXPANDABLE_COL && row.hasOwnProperty('isOpen')) {
+ row.isOpen = !row.isOpen;
+ }
+ return row;
+ })
+ }));
+ }
+ };
+
+ // depth max 1 in this example
+ getParentId = (rowId, rows) => (rows[rowId].parent === undefined ? rowId : rows[rowId].parent);
+ getChildIdId = (rowId, rows) =>
+ rows[rowId].parent === undefined
+ ? rows.map((row, idx) => (row.parent === rowId ? idx : null)).find(idx => idx !== null)
+ : rowId;
+
+ onEditActionClick = (event, rowId) => {
+ this.setState(({ rows, editedRowBackup }) => {
+ if (!editedRowBackup) {
+ const childId = this.getChildIdId(rowId, rows);
+ const parentId = this.getParentId(rowId, rows);
+
+ const backup = rows[childId].isEditableTogetherWithParent
+ ? {
+ [parentId]: rows[parentId],
+ [childId]: rows[childId]
+ }
+ : {
+ [rowId]: rows[rowId]
+ };
+
+ return {
+ editedRowsBackup: JSON.parse(JSON.stringify(backup)), // clone
+ rows: rows.map((row, id) => {
+ row.isEditing = !!backup[id];
+ return row;
+ })
+ };
+ }
+ return undefined;
+ });
+ };
+
+ onEditConfirmed = () => {
+ this.setState(({ rows, editedRowsBackup }) => {
+ rows = [...rows];
+ Object.keys(editedRowsBackup).forEach(key => {
+ rows[key].isEditing = false;
+ });
+
+ return {
+ rows,
+ editedRowsBackup: null,
+ activeEditId: null
+ };
+ });
+ };
+
+ onEditCanceled = () => {
+ this.setState(({ rows, editedRowsBackup }) => {
+ rows = [...rows];
+ Object.keys(editedRowsBackup).forEach(key => {
+ rows[key] = editedRowsBackup[key];
+ });
+ return {
+ rows,
+ editedRowsBackup: null,
+ activeEditId: null
+ };
+ });
+ };
+
+ onCollapse = (event, rowKey, isOpen) => {
+ const { rows } = this.state;
+ /**
+ * Please do not use rowKey as row index for more complex tables.
+ * Rather use some kind of identifier like ID passed with each row.
+ */
+ rows[rowKey].isOpen = isOpen;
+ this.setState({
+ rows
+ });
+ };
+
+ actionResolver = rowData =>
+ rowData.isTableEditing
+ ? null
+ : [
+ {
+ title: 'Edit',
+ onClick: this.onEditActionClick
+ }
+ ];
+
+ render() {
+ const { activeEditId, columns, rows } = this.state;
+ const editConfig = {
+ activeEditId,
+ onEditCellClicked: this.onEditCellClicked,
+ editConfirmationType: TableEditConfirmation.ROW,
+ onEditConfirmed: this.onEditConfirmed,
+ onEditCanceled: this.onEditCanceled
+ };
+ const ComposedBody = editableTableBody(TableBody);
+ const ComposedRowWrapper = editableRowWrapper(RowWrapper);
+
+ return (
+
+ );
+ }
+}
+
+export default CollapsibleEditableTable;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/examples/EditableTable.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/examples/EditableTable.js
new file mode 100644
index 00000000000..32a296b69db
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/examples/EditableTable.js
@@ -0,0 +1,306 @@
+import React from 'react';
+import { Table, TableHeader, TableBody, RowWrapper } from '@patternfly/react-table';
+import {
+ editableTableBody,
+ editableRowWrapper,
+ inlineEditFormatterFactory,
+ TableEditConfirmation,
+ TableTextInput
+} from '@patternfly/react-inline-edit-extension';
+import { Dropdown, DropdownToggle, DropdownItem, Checkbox } from '@patternfly/react-core';
+
+class EditableTable extends React.Component {
+ makeId = ({ column, rowIndex, columnIndex, name }) =>
+ `${column.property}-${rowIndex}-${columnIndex}${name ? `-${name}` : ''}`;
+
+ constructor(props) {
+ super(props);
+
+ // text input
+ const textInputFormatter = inlineEditFormatterFactory({
+ renderEdit: (value, { columnIndex, rowIndex, column }, { activeEditId }) => {
+ const id = this.makeId({ rowIndex, columnIndex, column });
+ return (
+
+ this.onChange(newValue, {
+ rowIndex,
+ columnIndex
+ })
+ }
+ autoFocus={activeEditId === id}
+ />
+ );
+ }
+ });
+
+ // dropdown
+ 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
+ });
+
+ // checkbox
+ const privateRepoFormatter = inlineEditFormatterFactory({
+ resolveValue: (value, { rowData }) => rowData.data.privateRepo,
+ renderEdit: (privateRepo, { column, columnIndex, rowIndex }) => (
+
+ this.onPrivateRepoChange(value, {
+ rowIndex,
+ columnIndex
+ })
+ }
+ aria-label="checkbox"
+ />
+ ),
+ renderValue: (privateRepo, { columnIndex, rowIndex, column }) => (
+
+ )
+ });
+
+ 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', null],
+ data: {
+ branches: ['master'],
+ workspace: {
+ selected: 'Green',
+ isDropdownOpen: false
+ },
+ privateRepo: false
+ }
+ // isEditing: true,
+ },
+ {
+ cells: ['', null, 0, null, '', null],
+ data: {
+ branches: ['master', 'v0.7.0', 'v1.0.0'],
+ workspace: {
+ selected: 'Grey',
+ isDropdownOpen: false
+ },
+ privateRepo: false
+ }
+ },
+ {
+ cells: ['p', null, 0, null, '', null],
+ data: {
+ branches: ['master', 'v0.7.0'],
+ workspace: {
+ selected: 'Orange',
+ isDropdownOpen: false
+ },
+ privateRepo: true
+ }
+ }
+ ],
+ editedRowBackup: null,
+ activeEditId: null
+ };
+ }
+
+ onPrivateRepoChange = (value, { rowIndex }) => {
+ this.setState(({ rows }) => {
+ const row = rows[rowIndex];
+ row.data.privateRepo = value;
+ return { rows };
+ });
+ };
+
+ onWorkspaceChange = (value, { rowIndex, columnIndex }) => {
+ this.setState(({ rows, activeEditId }) => {
+ const row = rows[rowIndex];
+ row.data.workspace = Object.assign({}, row.data.workspace, value);
+ if (value.isDropdownOpen) {
+ activeEditId = this.makeId({ rowIndex, columnIndex, column: { property: 'workspaces' }, name: 'dropdown' });
+ }
+ return { rows, activeEditId };
+ });
+ };
+
+ onChange = (value, { rowIndex, columnIndex }) => {
+ this.setState(({ rows }) => {
+ rows = [...rows];
+ const row = rows[rowIndex];
+ row.cells[columnIndex] = value;
+ return {
+ rows,
+ activeEditId: null // stop autoFocus
+ };
+ });
+ };
+
+ onEditCellClicked = (event, clickedRow, { rowIndex, columnIndex, elementId }) => {
+ const WORKSPACE_COL = 3;
+ const PRIVATE_REPO_COL = 5;
+ const ACTIONS_COL = 6;
+
+ if (elementId !== this.state.activeEditId && clickedRow.isEditing && columnIndex !== ACTIONS_COL) {
+ this.setState(({ rows }) => {
+ // focus dropdown if it opens ('toggle' is mistakenly assumed to be the elementId)
+ const activeEditId =
+ elementId && columnIndex === WORKSPACE_COL && !rows[rowIndex].data.workspace.isDropdownOpen
+ ? this.makeId({
+ rowIndex,
+ columnIndex,
+ column: { property: 'workspaces' },
+ name: 'dropdown'
+ })
+ : elementId;
+
+ return {
+ activeEditId,
+ rows: rows.map((row, id) => {
+ if (id === rowIndex) {
+ if (elementId && columnIndex === WORKSPACE_COL) {
+ row.data.workspace.isDropdownOpen = !row.data.workspace.isDropdownOpen;
+ } else {
+ if (elementId && columnIndex === PRIVATE_REPO_COL) {
+ row.data.privateRepo = !row.data.privateRepo;
+ }
+ row.data.workspace.isDropdownOpen = false;
+ }
+ }
+ 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,
+ activeEditId: null
+ };
+ });
+ };
+
+ onEditCanceled = (event, clickedRow, { rowIndex }) => {
+ this.setState(({ rows, editedRowBackup }) => {
+ rows = [...rows];
+ rows[rowIndex] = editedRowBackup;
+ return {
+ rows,
+ editedRowBackup: null,
+ activeEditId: null
+ };
+ });
+ };
+
+ actionResolver = rowData =>
+ rowData.isTableEditing
+ ? null
+ : [
+ {
+ title: 'Edit',
+ onClick: this.onEditActionClick
+ }
+ ];
+
+ render() {
+ const { columns, rows, activeEditId } = this.state;
+ const editConfig = {
+ activeEditId,
+ onEditCellClicked: this.onEditCellClicked,
+ editConfirmationType: TableEditConfirmation.ROW,
+ onEditConfirmed: this.onEditConfirmed,
+ onEditCanceled: this.onEditCanceled
+ };
+
+ const ComposedBody = editableTableBody(TableBody);
+ const ComposedRowWrapper = editableRowWrapper(RowWrapper);
+
+ return (
+
+ );
+ }
+}
+
+export default EditableTable;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/examples/EditableTableColumn.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/examples/EditableTableColumn.js
new file mode 100644
index 00000000000..017479906e6
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/examples/EditableTableColumn.js
@@ -0,0 +1,158 @@
+/* eslint-disable react/no-unused-state */
+import React from 'react';
+import { Table, TableHeader, TableBody, RowWrapper, TableVariant } from '@patternfly/react-table';
+import {
+ editableTableBody,
+ editableRowWrapper,
+ inlineEditFormatterFactory,
+ TableEditConfirmation,
+ TableTextInput
+} from '@patternfly/react-inline-edit-extension';
+
+class EditableTableColumn extends React.Component {
+ makeId = ({ column, rowIndex, columnIndex, name }) =>
+ `${column.property}-${rowIndex}-${columnIndex}${name ? `-${name}` : ''}`;
+
+ constructor(props) {
+ super(props);
+
+ // text input
+ const inlineEditingFormatter = inlineEditFormatterFactory({
+ renderEdit: (value, { columnIndex, rowIndex, column }, { activeEditId }) => {
+ const id = this.makeId({ rowIndex, columnIndex, column });
+ return (
+
+ this.onBlur(newValue, {
+ rowIndex,
+ columnIndex
+ })
+ }
+ autoFocus={activeEditId === id}
+ />
+ );
+ },
+ 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,
+ activeEditId: null
+ };
+ }
+
+ onBlur = (value, { rowIndex, columnIndex }) => {
+ this.setState(({ rows }) => {
+ rows = [...rows];
+ const row = rows[rowIndex];
+ row.cells[columnIndex] = value;
+ return {
+ rows,
+ activeEditId: null // stop autoFocus
+ };
+ });
+ };
+
+ onEditCellClicked = (event, clickedRow, { rowIndex, columnIndex, elementId }) => {
+ if (elementId !== this.state.activeEditId) {
+ this.setState({
+ activeEditId: elementId
+ });
+ }
+ };
+
+ setEditing = (rows, isEditing) =>
+ rows.map(row => {
+ row.isEditing = isEditing;
+ 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,
+ activeEditId: null
+ }));
+ };
+
+ onEditCanceled = () => {
+ this.setState(({ rows, rowsBackup }) => ({
+ rows: rowsBackup,
+ rowsBackup: null,
+ activeEditId: null
+ }));
+ };
+
+ render() {
+ const { columns, rows, activeEditId } = this.state;
+ const editConfig = {
+ activeEditId,
+ editConfirmationType: TableEditConfirmation.TABLE_BOTTOM,
+ onEditCellClicked: this.onEditCellClicked,
+ onEditConfirmed: this.onEditConfirmed,
+ onEditCanceled: this.onEditCanceled
+ };
+
+ const ComposedBody = editableTableBody(TableBody);
+ const ComposedRowWrapper = editableRowWrapper(RowWrapper);
+
+ return (
+
+ );
+ }
+}
+
+export default EditableTableColumn;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/index.d.ts b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/index.d.ts
new file mode 100644
index 00000000000..11364e4abef
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/index.d.ts
@@ -0,0 +1,19 @@
+export {
+ TableEditConfirmation
+} from './constants';
+
+export * from './utils';
+export {
+ default as editableTableBody,
+ EditableTableBody,
+ InlineEditBodyProps,
+ EditConfig,
+ IEditedCellData,
+} from './editableTableBody'
+
+export {
+ default as editableRowWrapper,
+ EditableRowWrapper,
+ EditableRowWrapperProps,
+ EditableRowWrapperRow
+} from './editableRowWrapper'
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/index.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/index.js
new file mode 100644
index 00000000000..22cf35095f6
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/index.js
@@ -0,0 +1,4 @@
+export { TableEditConfirmation } from './constants';
+export { inlineEditFormatterFactory } from './utils';
+export { default as editableTableBody } from './editableTableBody';
+export { default as editableRowWrapper } from './editableRowWrapper';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters.js
new file mode 100644
index 00000000000..6129309cae6
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters.js
@@ -0,0 +1 @@
+export { default as inlineEditFormatterFactory } from './formatters/inlineEditFormatterFactory';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/index.d.ts b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/index.d.ts
new file mode 100644
index 00000000000..102d7711d7e
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/index.d.ts
@@ -0,0 +1,8 @@
+export {
+ default as inlineEditFormatterFactory,
+ InlineEditFormatterFactory,
+ InlineEditFormatterFactoryProps,
+ AdditionalData,
+ Formatter,
+ FormatterExtra
+} from './inlineEditFormatterFactory';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/inlineEditFormatterFactory.d.ts b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/inlineEditFormatterFactory.d.ts
new file mode 100644
index 00000000000..810c3c6dd14
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/inlineEditFormatterFactory.d.ts
@@ -0,0 +1,30 @@
+import { ReactNode } from 'react';
+
+export interface AdditionalData {
+ columnIndex: number;
+ rowIndex: number;
+ column: Object;
+ rowData: Object;
+ rowKey: string;
+}
+
+export interface ComputedData {
+ activeEditId: string;
+}
+
+export interface Formatter {
+ (value: ReactNode, additionalData: AdditionalData): ReactNode;
+}
+
+export interface InlineEditFormatterFactoryProps {
+ isEditable?(additionalData: AdditionalData): boolean;
+ renderValue?: Formatter;
+ resolveValue?(value: ReactNode, additionalData: AdditionalData): ReactNode;
+ renderEdit?(value: ReactNode, additionalData: AdditionalData, computedData: ComputedData): ReactNode;
+}
+
+export interface InlineEditFormatterFactory {
+ (props: InlineEditFormatterFactoryProps): Formatter;
+}
+
+export default InlineEditFormatterFactory;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/inlineEditFormatterFactory.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/inlineEditFormatterFactory.js
new file mode 100644
index 00000000000..26f5ebf8ecd
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/inlineEditFormatterFactory.js
@@ -0,0 +1,23 @@
+const inlineEditFormatterFactory = ({ renderEdit, renderValue, resolveValue, isEditable = null } = {}) => (
+ value,
+ additionalData
+) => {
+ const { rowData } = additionalData;
+
+ if (resolveValue) {
+ value = resolveValue(value, additionalData);
+ }
+
+ if (renderEdit && rowData.isEditing && (!isEditable || isEditable(additionalData))) {
+ const computedData = {
+ activeEditId: rowData.editConfig && rowData.editConfig.activeEditId
+ };
+ return renderEdit(value, additionalData, computedData);
+ } else if (renderValue) {
+ return renderValue(value, additionalData);
+ }
+
+ return value;
+};
+
+export default inlineEditFormatterFactory;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/inlineEditFormatterFactory.test.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/inlineEditFormatterFactory.test.js
new file mode 100644
index 00000000000..c203af249e9
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/formatters/inlineEditFormatterFactory.test.js
@@ -0,0 +1,65 @@
+import { default as inlineEditFormatterFactory } from './inlineEditFormatterFactory';
+
+const blue = 'blue';
+const alteredValue = 'violet';
+
+const buildAdditionalData = (data, isEditing = true, activeEditId = 'testId') => ({
+ rowData: {
+ isEditing,
+ editConfig: {
+ activeEditId
+ },
+ data
+ }
+});
+
+describe('inlineEditFormatterFactory', () => {
+ test('renders default value', () => {
+ const additionalData = buildAdditionalData(alteredValue);
+
+ expect(inlineEditFormatterFactory()(blue, additionalData)).toBe(blue);
+ });
+
+ test('renders resolved value', () => {
+ const additionalData = buildAdditionalData(alteredValue);
+
+ expect(
+ inlineEditFormatterFactory({
+ resolveValue: (value, { rowData }) => rowData.data
+ })(blue, additionalData)
+ ).toBe(alteredValue);
+ });
+
+ test('renders resolved and render value', () => {
+ const additionalData = buildAdditionalData(alteredValue, false);
+
+ expect(
+ inlineEditFormatterFactory({
+ resolveValue: (value, { rowData }) => rowData.data,
+ renderValue: value => `ultra ${value}`,
+ renderEdit: value => `violent ${value}`
+ })(blue, additionalData)
+ ).toBe(`ultra ${alteredValue}`);
+ });
+
+ test('renders resolved and render edit value', () => {
+ const additionalData = buildAdditionalData(alteredValue);
+
+ expect(
+ inlineEditFormatterFactory({
+ resolveValue: (value, { rowData }) => rowData.data,
+ renderEdit: value => `ultra ${value}`
+ })(blue, additionalData)
+ ).toBe(`ultra ${alteredValue}`);
+ });
+
+ test('passes computed data', () => {
+ const additionalData = buildAdditionalData(alteredValue, true, 'myId');
+
+ expect(
+ inlineEditFormatterFactory({
+ renderEdit: (value, data, { activeEditId }) => activeEditId
+ })(blue, additionalData)
+ ).toBe('myId');
+ });
+});
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/index.d.ts b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/index.d.ts
new file mode 100644
index 00000000000..ad4ce58d7f7
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/index.d.ts
@@ -0,0 +1 @@
+export * from './formatters';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/index.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/index.js
new file mode 100644
index 00000000000..3df838ed640
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/index.js
@@ -0,0 +1,2 @@
+export * from './formatters';
+export * from './utils';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/utils.js b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/utils.js
new file mode 100644
index 00000000000..1df8d58f692
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/InlineEdit/utils/utils.js
@@ -0,0 +1,39 @@
+const requiredElements = ['INPUT', 'BUTTON'];
+let warningCompleted = false;
+
+export const showIdWarnings = (row, target) => {
+ if ((row.isEditing || row.isTableEditing) && requiredElements.indexOf(target.tagName) >= 0 && !warningCompleted) {
+ warningCompleted = true;
+ // eslint-disable-next-line no-console
+ console.warn(`${requiredElements.join(', ')} elements are required to have an id for editing to work properly.`);
+ }
+};
+
+export const combineFunctions = (...functions) => {
+ const realFunctions = functions.filter(f => typeof f === 'function');
+
+ if (realFunctions.length === 1) {
+ return realFunctions[0];
+ }
+
+ if (realFunctions.length > 1) {
+ return (...params) => {
+ realFunctions.forEach(f => f(...params));
+ };
+ }
+
+ return undefined;
+};
+
+export const shallowLeftSideEquals = (newObject, oldObject) =>
+ !Object.keys(newObject).find(key => newObject[key] !== oldObject[key]);
+
+export const getBoundingClientRect = element => {
+ const { top, right, bottom, left, width, height, x, y } = element.getBoundingClientRect();
+ return { top, right, bottom, left, width, height, x, y };
+};
+
+export const getClientWindowDimensions = () => {
+ const { clientWidth, clientHeight } = document.documentElement;
+ return { width: clientWidth, height: clientHeight };
+};
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/TableTextInput.d.ts b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/TableTextInput.d.ts
new file mode 100644
index 00000000000..5dff6d378cf
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/TableTextInput.d.ts
@@ -0,0 +1,12 @@
+import { SFC, FormEvent } from 'react';
+import { TextInputProps, Omit } from '@patternfly/react-core';
+
+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-inline-edit-extension/src/components/TableTextInput/TableTextInput.docs.js b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/TableTextInput.docs.js
new file mode 100644
index 00000000000..e17470841c8
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/TableTextInput.docs.js
@@ -0,0 +1,10 @@
+import { TableTextInput } from '@patternfly/react-inline-edit-extension';
+import SimpleTableTextInput from './examples/SimpleTableTextInput';
+
+export default {
+ title: 'TableTextInput',
+ components: {
+ TableTextInput
+ },
+ examples: [{ component: SimpleTableTextInput, title: 'Simple TableTextInput' }]
+};
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/TableTextInput.js b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/TableTextInput.js
new file mode 100644
index 00000000000..b0ffeb81f0e
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/TableTextInput.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import { TextInput } from '@patternfly/react-core';
+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-inline-edit-extension/src/components/TableTextInput/TableTextInput.test.js b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/TableTextInput.test.js
new file mode 100644
index 00000000000..40850c59566
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/TableTextInput.test.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import TableTextInput from './TableTextInput';
+
+const props = {
+ defaultValue: 'test',
+ autoFocus: true,
+ onBlur: jest.fn()
+};
+
+test('simple table text input', () => {
+ const view = shallow();
+ expect(view).toMatchSnapshot();
+});
+
+test('focused table text input', () => {
+ const view = shallow();
+ expect(view).toMatchSnapshot();
+});
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/__snapshots__/TableTextInput.test.js.snap b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/__snapshots__/TableTextInput.test.js.snap
new file mode 100644
index 00000000000..c8825af6b08
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/__snapshots__/TableTextInput.test.js.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`focused table text input 1`] = `
+
+
+
+`;
+
+exports[`simple table text input 1`] = `
+
+
+
+`;
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/examples/SimpleTableTextInput.js b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/examples/SimpleTableTextInput.js
new file mode 100644
index 00000000000..d8e76d28cc3
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/examples/SimpleTableTextInput.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import { TableTextInput } from '@patternfly/react-inline-edit-extension';
+
+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-inline-edit-extension/src/components/TableTextInput/index.d.ts b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/index.d.ts
new file mode 100644
index 00000000000..5d54cba1787
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/index.d.ts
@@ -0,0 +1,4 @@
+export {
+ default as TableTextInput,
+ TableTextInputProps
+} from './TableTextInput';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/index.js b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/index.js
new file mode 100644
index 00000000000..eab02a44ac5
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/TableTextInput/index.js
@@ -0,0 +1 @@
+export { default as TableTextInput } from './TableTextInput';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/index.d.ts b/packages/patternfly-4/react-inline-edit-extension/src/components/index.d.ts
new file mode 100644
index 00000000000..61799229f7b
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/index.d.ts
@@ -0,0 +1,2 @@
+export * from './InlineEdit';
+export * from './TableTextInput';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/components/index.js b/packages/patternfly-4/react-inline-edit-extension/src/components/index.js
new file mode 100644
index 00000000000..61799229f7b
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/components/index.js
@@ -0,0 +1,2 @@
+export * from './InlineEdit';
+export * from './TableTextInput';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/index.d.ts b/packages/patternfly-4/react-inline-edit-extension/src/index.d.ts
new file mode 100644
index 00000000000..07635cbbc8e
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/index.d.ts
@@ -0,0 +1 @@
+export * from './components';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/index.js b/packages/patternfly-4/react-inline-edit-extension/src/index.js
new file mode 100644
index 00000000000..07635cbbc8e
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/index.js
@@ -0,0 +1 @@
+export * from './components';
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/test-helpers/data-sets.js b/packages/patternfly-4/react-inline-edit-extension/src/test-helpers/data-sets.js
new file mode 100644
index 00000000000..a0b4b314664
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/test-helpers/data-sets.js
@@ -0,0 +1,39 @@
+export const columns = [
+ { title: 'Header cell' },
+ 'Branches',
+ { title: 'Pull requests' },
+ 'Workspaces',
+ {
+ title: 'Last Commit'
+ }
+];
+
+export const rows = [
+ {
+ cells: ['one', 'two', 'three', 'four', 'five']
+ },
+ {
+ cells: ['one', 'two', 'three', 'four', 'five']
+ },
+ {
+ cells: ['one', 'two', 'three', 'four', 'five']
+ },
+ {
+ cells: ['one', 'two', 'three', 'four', 'five']
+ },
+ {
+ cells: ['one', 'two', 'three', 'four', 'five']
+ },
+ {
+ cells: ['one', 'two', 'three', 'four', 'five']
+ },
+ {
+ cells: ['one', 'two', 'three', 'four', 'five']
+ },
+ {
+ cells: ['one', 'two', 'three', 'four', 'five']
+ },
+ {
+ cells: ['one', 'two', 'three', 'four', 'five']
+ }
+];
diff --git a/packages/patternfly-4/react-inline-edit-extension/src/test-helpers/helpers.js b/packages/patternfly-4/react-inline-edit-extension/src/test-helpers/helpers.js
new file mode 100644
index 00000000000..0e3df6f7fe5
--- /dev/null
+++ b/packages/patternfly-4/react-inline-edit-extension/src/test-helpers/helpers.js
@@ -0,0 +1,8 @@
+export const mockClosest = (map, resolveFunction, force = false) => {
+ if (!Element.prototype.closest || force) {
+ Element.prototype.closest = selector => (map && map[selector]) || (resolveFunction && resolveFunction(selector));
+ }
+};
+
+export const makeTableId = ({ column: { property }, rowIndex, columnIndex, name }) =>
+ `${property}-${rowIndex}-${columnIndex}${name ? `-${name}` : ''}`;
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 1df97d8d593..b658b32c0ea 100644
--- a/packages/patternfly-4/react-table/src/components/Table/Body.js
+++ b/packages/patternfly-4/react-table/src/components/Table/Body.js
@@ -19,16 +19,26 @@ const defaultProps = {
onRowClick: () => undefined
};
+const flagVisibility = rows => {
+ const visibleRows = rows.filter(oneRow => !oneRow.parent || oneRow.isExpanded);
+ if (visibleRows.length > 0) {
+ visibleRows[0].isFirstVisible = true;
+ visibleRows[visibleRows.length - 1].isLastVisible = true;
+ }
+};
+
class ContextBody extends React.Component {
- onRow = (row, props) => {
+ onRow = (row, rowProps) => {
const { onRowClick } = this.props;
return {
- isExpanded: row.isExpanded,
- isOpen: row.isOpen,
- onClick: event => {
- if (event.target.tagName !== 'INPUT' && event.target.tagName !== 'BUTTON') {
- onRowClick(event, row, props);
- }
+ row,
+ rowProps,
+ onMouseDown: event => {
+ const computedData = {
+ isInput: event.target.tagName !== 'INPUT',
+ isButton: event.target.tagName !== 'BUTTON'
+ };
+ onRowClick(event, row, rowProps, computedData);
}
};
};
@@ -69,13 +79,20 @@ class ContextBody extends React.Component {
render() {
const { className, headerData, rows, rowKey, children, onRowClick, ...props } = this.props;
- const mappedRows =
- headerData.length > 0 &&
- rows.map((oneRow, oneRowKey) => ({
+
+ let mappedRows;
+ if (headerData.length > 0) {
+ mappedRows = rows.map((oneRow, oneRowKey) => ({
...oneRow,
...this.mapCells(headerData, oneRow, oneRowKey),
- isExpanded: isRowExpanded(oneRow, rows)
+ isExpanded: isRowExpanded(oneRow, rows),
+ isFirst: oneRowKey === 0,
+ isLast: oneRowKey === rows.length - 1,
+ isFirstVisible: false,
+ isLastVisible: false
}));
+ flagVisibility(mappedRows);
+ }
return (
@@ -96,7 +113,7 @@ class ContextBody extends React.Component {
const TableBody = props => (
- {({ headerData, rows }) => }
+ {({ headerData, rows }) => }
);
diff --git a/packages/patternfly-4/react-table/src/components/Table/RowWrapper.d.ts b/packages/patternfly-4/react-table/src/components/Table/RowWrapper.d.ts
new file mode 100644
index 00000000000..f0b070b1e55
--- /dev/null
+++ b/packages/patternfly-4/react-table/src/components/Table/RowWrapper.d.ts
@@ -0,0 +1,16 @@
+import { UIEventHandler } from 'react';
+
+export interface RowWrapperRow {
+ isOpen: Boolean;
+ isExpanded: Boolean;
+}
+
+export interface RowWrapper {
+ trRef?: Function;
+ onScroll?: UIEventHandler;
+ onResize?: UIEventHandler;
+ row?: RowWrapperRow;
+ rowProps?: Object;
+}
+
+export default RowWrapper;
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 fb26feb314b..6cbd20df65a 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,103 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { debounce } from '@patternfly/react-core';
import styles from '@patternfly/patternfly/components/Table/table.css';
import { css } from '@patternfly/react-styles';
-const RowWrapper = ({ isOpen, isExpanded, ...props }) => (
-
-);
+class RowWrapper extends React.Component {
+ constructor(props) {
+ super(props);
+
+ if (props.onScroll) {
+ this.handleScroll = debounce(this.handleScroll, 100);
+ }
+ if (props.onResize) {
+ this.handleResize = debounce(this.handleResize, 100);
+ }
+ }
+
+ componentDidMount() {
+ this._unmounted = false;
+
+ if (this.props.onScroll) {
+ window.addEventListener('scroll', this.handleScroll);
+ }
+ if (this.props.onResize) {
+ window.addEventListener('resize', this.handleResize);
+ }
+ }
+
+ componentWillUnmount() {
+ this._unmounted = true;
+
+ if (this.props.onScroll) {
+ window.removeEventListener('scroll', this.handleScroll);
+ }
+ if (this.props.onResize) {
+ window.removeEventListener('resize', this.handleResize);
+ }
+ }
+
+ handleScroll = event => {
+ if (!this._unmounted) {
+ this.props.onScroll(event);
+ }
+ };
+
+ handleResize = event => {
+ if (!this._unmounted) {
+ this.props.onResize(event);
+ }
+ };
+
+ render() {
+ const {
+ trRef,
+ className,
+ onScroll,
+ onResize,
+ row: { isExpanded },
+ rowProps,
+ ...props
+ } = this.props;
+
+ return (
+
+ );
+ }
+}
RowWrapper.propTypes = {
- isOpen: PropTypes.bool,
- isExpanded: PropTypes.bool
+ trRef: PropTypes.func,
+ className: PropTypes.string,
+ onScroll: PropTypes.func,
+ onResize: PropTypes.func,
+ row: PropTypes.shape({
+ isOpen: PropTypes.bool,
+ isExpanded: PropTypes.bool
+ }),
+ rowProps: PropTypes.object
};
RowWrapper.defaultProps = {
- isOpen: undefined,
- isExpanded: undefined
+ trRef: undefined,
+ className: '',
+ onScroll: undefined,
+ onResize: undefined,
+ row: {
+ isOpen: undefined,
+ isExpanded: undefined
+ },
+ rowProps: null
};
export default RowWrapper;
diff --git a/packages/patternfly-4/react-table/src/components/Table/RowWrapper.test.js b/packages/patternfly-4/react-table/src/components/Table/RowWrapper.test.js
new file mode 100644
index 00000000000..08b3f3fa5ee
--- /dev/null
+++ b/packages/patternfly-4/react-table/src/components/Table/RowWrapper.test.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import RowWrapper from './RowWrapper';
+
+const getRowWrapper = props => (
+
+);
+
+describe('RowWrapper', () => {
+ test('renders correctly', () => {
+ const trRef = jest.fn();
+ const view = mount(getRowWrapper({ onScroll: jest.fn(), onResize: jest.fn(), trRef }));
+ expect(view).toMatchSnapshot();
+ expect(trRef.mock.calls).toHaveLength(1);
+ });
+ test('renders expanded correctly', () => {
+ const view = mount(getRowWrapper({ row: { isExpanded: true } }));
+ expect(view).toMatchSnapshot();
+ });
+});
diff --git a/packages/patternfly-4/react-table/src/components/Table/Table.d.ts b/packages/patternfly-4/react-table/src/components/Table/Table.d.ts
index f77bcebd135..00db3aad6f3 100644
--- a/packages/patternfly-4/react-table/src/components/Table/Table.d.ts
+++ b/packages/patternfly-4/react-table/src/components/Table/Table.d.ts
@@ -26,14 +26,18 @@ export interface IColumn {
}
}
+export interface IExtraRowData {
+ rowIndex: number;
+ rowKey?: string;
+}
+
export interface IExtraColumnData {
columnIndex: number,
column: IColumn,
property: string,
}
-export interface IExtraData extends IExtraColumnData {
- rowIndex: number
+export interface IExtraData extends IExtraColumnData, IExtraRowData {
}
export interface IExtra extends IExtraData {
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 ba41eb1bea2..3a1a4d58cd4 100644
--- a/packages/patternfly-4/react-table/src/components/Table/Table.js
+++ b/packages/patternfly-4/react-table/src/components/Table/Table.js
@@ -51,8 +51,12 @@ const propTypes = {
),
/** Function should resolve an array of actions for each row in the same format as actions. */
actionResolver: PropTypes.func,
- /** Function should resolve if action is disabled for each row */
+ /** Function should resolve if action kebap is disabled for each row */
areActionsDisabled: PropTypes.func,
+ /** Override to the default BodyWrapper renderer */
+ bodyWrapper: PropTypes.func,
+ /** Override to the default RowWrapper renderer */
+ rowWrapper: PropTypes.func,
/** Actual rows to display in table. Either array of strings or row objects.
* If you want to use components in row cells you can pass them as title prop in cells definition.
* Ex: rows:[
@@ -188,6 +192,8 @@ class Table extends React.Component {
variant,
rows,
cells,
+ bodyWrapper,
+ rowWrapper,
...props
} = this.props;
@@ -220,8 +226,8 @@ class Table extends React.Component {
{...props}
renderers={{
body: {
- wrapper: BodyWrapper,
- row: RowWrapper,
+ wrapper: bodyWrapper || BodyWrapper,
+ row: rowWrapper || RowWrapper,
cell: BodyCell
},
header: {
diff --git a/packages/patternfly-4/react-table/src/components/Table/__snapshots__/RowWrapper.test.js.snap b/packages/patternfly-4/react-table/src/components/Table/__snapshots__/RowWrapper.test.js.snap
new file mode 100644
index 00000000000..9103b6971ca
--- /dev/null
+++ b/packages/patternfly-4/react-table/src/components/Table/__snapshots__/RowWrapper.test.js.snap
@@ -0,0 +1,73 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RowWrapper renders correctly 1`] = `
+
+
+ ,
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ >
+
+
+
+
+`;
+
+exports[`RowWrapper renders expanded correctly 1`] = `
+.pf-c-table__expandable-row.pf-m-expanded {
+ display: block;
+ position: relative;
+ border-bottom: 0 solid transparent;
+ box-shadow: 0 0.3125rem 0.625rem -0.25rem rgba(3, 3, 3, 0.25);
+ transition: all 250ms cubic-bezier(0.42, 0, 0.58, 1);
+ border-bottom-color: #ededed;
+ border-bottom-width: 1px;
+}
+
+
+`;
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 d1e102a80b7..734b9b0008c 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
@@ -463,6 +463,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -503,6 +504,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -542,6 +544,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -581,6 +584,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -620,6 +624,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -660,6 +665,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -745,6 +751,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -785,6 +792,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -824,6 +832,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -863,6 +872,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -902,6 +912,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -942,6 +953,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -1128,6 +1140,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -1168,6 +1181,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -1207,6 +1221,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -1246,6 +1261,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -1285,6 +1301,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -1325,6 +1342,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -1472,6 +1490,10 @@ exports[`Actions table 1`] = `
},
"id": 0,
"isExpanded": undefined,
+ "isFirst": true,
+ "isFirstVisible": true,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1513,6 +1535,10 @@ exports[`Actions table 1`] = `
},
"id": 1,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1554,6 +1580,10 @@ exports[`Actions table 1`] = `
},
"id": 2,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1595,6 +1625,10 @@ exports[`Actions table 1`] = `
},
"id": 3,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1636,6 +1670,10 @@ exports[`Actions table 1`] = `
},
"id": 4,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1677,6 +1715,10 @@ exports[`Actions table 1`] = `
},
"id": 5,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1718,6 +1760,10 @@ exports[`Actions table 1`] = `
},
"id": 6,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1759,6 +1805,10 @@ exports[`Actions table 1`] = `
},
"id": 7,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1800,6 +1850,10 @@ exports[`Actions table 1`] = `
},
"id": 8,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": true,
+ "isLastVisible": true,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1847,6 +1901,10 @@ exports[`Actions table 1`] = `
},
"id": 0,
"isExpanded": undefined,
+ "isFirst": true,
+ "isFirstVisible": true,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1888,6 +1946,10 @@ exports[`Actions table 1`] = `
},
"id": 1,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1929,6 +1991,10 @@ exports[`Actions table 1`] = `
},
"id": 2,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -1970,6 +2036,10 @@ exports[`Actions table 1`] = `
},
"id": 3,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2011,6 +2081,10 @@ exports[`Actions table 1`] = `
},
"id": 4,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2052,6 +2126,10 @@ exports[`Actions table 1`] = `
},
"id": 5,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2093,6 +2171,10 @@ exports[`Actions table 1`] = `
},
"id": 6,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2134,6 +2216,10 @@ exports[`Actions table 1`] = `
},
"id": 7,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2175,6 +2261,10 @@ exports[`Actions table 1`] = `
},
"id": 8,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": true,
+ "isLastVisible": true,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2223,6 +2313,10 @@ exports[`Actions table 1`] = `
},
"id": 0,
"isExpanded": undefined,
+ "isFirst": true,
+ "isFirstVisible": true,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2264,6 +2358,10 @@ exports[`Actions table 1`] = `
},
"id": 1,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2305,6 +2403,10 @@ exports[`Actions table 1`] = `
},
"id": 2,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2346,6 +2448,10 @@ exports[`Actions table 1`] = `
},
"id": 3,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2387,6 +2493,10 @@ exports[`Actions table 1`] = `
},
"id": 4,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2428,6 +2538,10 @@ exports[`Actions table 1`] = `
},
"id": 5,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2469,6 +2583,10 @@ exports[`Actions table 1`] = `
},
"id": 6,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2510,6 +2628,10 @@ exports[`Actions table 1`] = `
},
"id": 7,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2551,6 +2673,10 @@ exports[`Actions table 1`] = `
},
"id": 8,
"isExpanded": undefined,
+ "isFirst": false,
+ "isFirstVisible": false,
+ "isLast": true,
+ "isLastVisible": true,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2590,6 +2716,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -2630,6 +2757,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -2669,6 +2797,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -2708,6 +2837,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -2747,6 +2877,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -2787,6 +2918,7 @@ exports[`Actions table 1`] = `
[Function],
],
},
+ "data": undefined,
"extraParams": Object {
"actionResolver": [Function],
"actions": undefined,
@@ -2852,6 +2984,10 @@ exports[`Actions table 1`] = `
},
"id": 0,
"isExpanded": undefined,
+ "isFirst": true,
+ "isFirstVisible": true,
+ "isLast": false,
+ "isLastVisible": false,
"last-commit": Object {
"props": Object {
"isVisible": true,
@@ -2876,12 +3012,66 @@ exports[`Actions table 1`] = `
rowKey="0-row"
>