From 619d68eb6604291df488c409e64129b4b0374dfe Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Mon, 18 Dec 2017 15:54:20 -0500 Subject: [PATCH] feat(Filter): Add Filter Component --- less/_filter.less | 36 ++ less/patternfly-react.less | 2 + package-lock.json | 180 ++++---- sass/patternfly-react/_filter.scss | 36 ++ sass/patternfly-react/_patternfly-react.scss | 2 + src/components/Filter/Filter.js | 389 ++++++++++++++++++ src/components/Filter/Filter.stories.js | 64 +++ src/components/Filter/Filter.test.js | 11 + .../Filter/__mocks__/mockFilterDisplay.js | 68 +++ .../Filter/__mocks__/mockFilterFields.js | 139 +++++++ .../Filter/__snapshots__/Filter.test.js.snap | 115 ++++++ src/components/Filter/index.js | 3 + src/index.js | 1 + 13 files changed, 956 insertions(+), 90 deletions(-) create mode 100644 less/_filter.less create mode 100644 sass/patternfly-react/_filter.scss create mode 100644 src/components/Filter/Filter.js create mode 100644 src/components/Filter/Filter.stories.js create mode 100644 src/components/Filter/Filter.test.js create mode 100644 src/components/Filter/__mocks__/mockFilterDisplay.js create mode 100644 src/components/Filter/__mocks__/mockFilterFields.js create mode 100644 src/components/Filter/__snapshots__/Filter.test.js.snap create mode 100644 src/components/Filter/index.js diff --git a/less/_filter.less b/less/_filter.less new file mode 100644 index 00000000000..dc4bff579ea --- /dev/null +++ b/less/_filter.less @@ -0,0 +1,36 @@ +.filter-pf { + .filter-pf-category-select { + display: flex; + } + .filter-pf-category-select-value { + border-left-width: 0; + } + + .filter-pf-category-item { + margin-bottom: 5px; + } + + .filter-pf-fields .form-group { + width: 275px; + } + .filter-pf-select-dropdown { + background-color: @color-pf-white; + background-image: none; + color: @color-pf-black-500; + font-size: 12px; + font-style: italic; + font-weight: 400; + padding-right: 25px; + text-align: left; + width: 100%; + z-index: 1; + } + .filter-pf-category-label { + font-weight: 700; + margin-right: 5px; + padding: 5px 0 6px 5px; + } + .filter-pf-select > .dropdown.btn-group { + width: 100%; + } +} diff --git a/less/patternfly-react.less b/less/patternfly-react.less index bbb5e95e066..09a179b0cf6 100644 --- a/less/patternfly-react.less +++ b/less/patternfly-react.less @@ -1,3 +1,5 @@ /** Patternfly React Specific Extensions */ + +@import '_filter'; diff --git a/package-lock.json b/package-lock.json index 353cb9e8e64..5ccb58a800e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,12 +94,12 @@ } }, "@storybook/addon-actions": { - "version": "3.2.17", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-3.2.17.tgz", - "integrity": "sha512-B4++4+p6zWTeTBzqe9lgzT8J0onaMfSZsBRJpa5URoDo3d4kTq+DTDs2qHhhTKJb2Drtu24/JlEgJG7lv0Fb0w==", + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-3.2.18.tgz", + "integrity": "sha512-jIbNxYXm3MyPECcPWZ9Vxuq31/rwq/oPXhfPpf4xD9sdQcfwm/5on2d75UJOXKio60xWagP4RpECz7d3uc6nJA==", "dev": true, "requires": { - "@storybook/addons": "3.2.17", + "@storybook/addons": "3.2.18", "deep-equal": "1.0.1", "json-stringify-safe": "5.0.1", "prop-types": "15.6.0", @@ -108,13 +108,13 @@ } }, "@storybook/addon-info": { - "version": "3.2.17", - "resolved": "https://registry.npmjs.org/@storybook/addon-info/-/addon-info-3.2.17.tgz", - "integrity": "sha512-MHZv/lam+NYyibOIRly+66GPSmlhrGMDlQaZFliXYILQuF6skN4KeC/A7IkhquAUVA2PrkBS8EJfTAV0Qw0pcQ==", + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@storybook/addon-info/-/addon-info-3.2.18.tgz", + "integrity": "sha512-InEJmMUgWHpP4O0Q+Wq74p/K28cNf6qcW6LriOkYvFqES6p1LdMJUvbRoblpQh6T5gI88weGVS50+wRDLBKCdw==", "dev": true, "requires": { - "@storybook/addons": "3.2.17", - "@storybook/components": "3.2.17", + "@storybook/addons": "3.2.18", + "@storybook/components": "3.2.18", "babel-runtime": "6.26.0", "global": "4.3.2", "marksy": "2.0.1", @@ -129,13 +129,13 @@ "integrity": "sha512-Xnd54kx47boM5Xl0sVMqblcki3Z8Z9hS8CIWz5CCJCfxQKx7mhFtcOmHrIqUH2oUZhhFSj+aFrRx1zPcFcPaZQ==", "dev": true, "requires": { - "@storybook/addons": "3.2.17", + "@storybook/addons": "3.2.18", "babel-runtime": "6.26.0", "deep-equal": "1.0.1", "global": "4.3.2", "insert-css": "1.1.0", "lodash.debounce": "4.0.8", - "moment": "2.19.4", + "moment": "2.20.1", "prop-types": "15.6.0", "react-color": "2.13.8", "react-datetime": "2.11.1", @@ -144,41 +144,41 @@ } }, "@storybook/addon-links": { - "version": "3.2.17", - "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-3.2.17.tgz", - "integrity": "sha512-AdCH9DsbOfiMDv42/l0Zfk2yhiBrlWuzHk7ubhSwqF3/61sXr+BWFv+SlOx2oFfSWAT7zfg2sC7Wr+us8gs7GQ==", + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-3.2.18.tgz", + "integrity": "sha512-X96TNRA0x0rWWVweIQCDM84GJdQpAXQutxnARvskV0RD6bf8Bhq7nw3FVzElNilnFyfhtFzYVqCUlEOAJcKEgg==", "dev": true, "requires": { - "@storybook/addons": "3.2.17" + "@storybook/addons": "3.2.18" } }, "@storybook/addons": { - "version": "3.2.17", - "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-3.2.17.tgz", - "integrity": "sha512-1/Ux++3hMfYqAgBwgWbGtGAM0CfSdAchf//wDLBUmX09+E5CjiQvW3YwVplNYzfRuAsSrE1GOYJRAsz639oTYQ==", + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-3.2.18.tgz", + "integrity": "sha512-I1oJhimBZiyrDa2UxorXhBBGapAIa+KxF8thtCUWmFgToeWhlHnRepQiRE+mCDM2yjSQpmTN+md/W8DPcLIHag==", "dev": true }, "@storybook/channel-postmessage": { - "version": "3.2.17", - "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-3.2.17.tgz", - "integrity": "sha512-sNlXcHTKM6aIxRsQMaMbowsAToYRlbeP/THqulVxoRRKNDPndNnZe/Lu3eSjBjAfNWBLRFfgX903adt82QAPCA==", + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-3.2.18.tgz", + "integrity": "sha512-a3SSLYlVf61BaAcM2pA/W4Rhdh/RCf1oise22TXfgC83ELUyRksKQX/Pm7FKn2NgffrZb5aRIUQRzfe3LH/www==", "dev": true, "requires": { - "@storybook/channels": "3.2.17", + "@storybook/channels": "3.2.18", "global": "4.3.2", "json-stringify-safe": "5.0.1" } }, "@storybook/channels": { - "version": "3.2.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-3.2.17.tgz", - "integrity": "sha512-HIdRmFTFVLcbrwYFf6+LyAlgcd57ki+6DDTmcvXQTHCWrOOCJKwfKjHgn6tbnHlGHiWByA8lAdO1bFcYhHxl4Q==", + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-3.2.18.tgz", + "integrity": "sha512-7+3Kic5FRWyojff/CA4wfhI24xxbXmQzdVLx0gUZm8r4+h/3JSt8hVfhJeXkS+GEvo852PRS/AOHnN4ENKw9uw==", "dev": true }, "@storybook/components": { - "version": "3.2.17", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-3.2.17.tgz", - "integrity": "sha512-pXwNKLavYCu18B2EynFu9EKXlKi0LDo0B/KctbJvidxR5ubzqkZu9h2ti3hPPVomR2DQiRx61Vzqd7cbcyy14w==", + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-3.2.18.tgz", + "integrity": "sha512-jvwUewMiwG+svDqLxu6YyHQdBdvDQ5etA5dUplpx7aPW4bZog+DHkZ0RLgwHadxbeff1qs/RXlz23D3El6BDxA==", "dev": true, "requires": { "glamor": "2.20.40", @@ -203,11 +203,11 @@ "integrity": "sha512-/TWoVYHfoUnEAjcEj463zRIA7SJjB6RIzShFzI2HtJw+P2EdT8mFN2EYGdsuGHQqkbGSC3owHSy1Jxuy1GVuqQ==", "dev": true, "requires": { - "@storybook/addon-actions": "3.2.17", - "@storybook/addon-links": "3.2.17", - "@storybook/addons": "3.2.17", - "@storybook/channel-postmessage": "3.2.17", - "@storybook/ui": "3.2.17", + "@storybook/addon-actions": "3.2.18", + "@storybook/addon-links": "3.2.18", + "@storybook/addons": "3.2.18", + "@storybook/channel-postmessage": "3.2.18", + "@storybook/ui": "3.2.18", "airbnb-js-shims": "1.4.0", "autoprefixer": "7.2.3", "babel-core": "6.26.0", @@ -343,13 +343,13 @@ } }, "@storybook/ui": { - "version": "3.2.17", - "resolved": "https://registry.npmjs.org/@storybook/ui/-/ui-3.2.17.tgz", - "integrity": "sha512-At0oWWTALJrJhtdnP+0AMANBC5omIBnEg3Pin3M0+yzUy6Poelcvt8+81dQEKLBP6LiyFOVpiDjfqpiHR5l0ow==", + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@storybook/ui/-/ui-3.2.18.tgz", + "integrity": "sha512-n3j//YF5gbTRZHrdIREGK8C+weaCF/pu4iWKu4Pty8rbMjrghkgrhENga2XaFvEOfTsBUc2ldXCCjNw0tJOGWg==", "dev": true, "requires": { "@hypnosphi/fuse.js": "3.0.9", - "@storybook/components": "3.2.17", + "@storybook/components": "3.2.18", "@storybook/mantra-core": "1.7.2", "@storybook/react-fuzzy": "0.4.3", "@storybook/react-komposer": "2.0.3", @@ -393,9 +393,9 @@ "dev": true }, "@types/react": { - "version": "16.0.30", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.0.30.tgz", - "integrity": "sha512-BQHUN9veeQJsmIod4i7sphZPAp85KBsYNtL2YMScPosJ58mVyChcN2SIfItmMrxZYWy2gBj5H3oKasT72rbsjQ==", + "version": "16.0.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.0.31.tgz", + "integrity": "sha512-ft7OuDGUo39e+9LGwUewf2RyEaNBOjWbHUmD5bzjNuSuDabccE/1IuO7iR0dkzLjVUKxTMq69E+FmKfbgBcfbQ==", "dev": true }, "abab": { @@ -504,7 +504,7 @@ "array-includes": "3.0.3", "array.prototype.flatmap": "1.1.1", "array.prototype.flatten": "1.1.1", - "es5-shim": "4.5.9", + "es5-shim": "4.5.10", "es6-shim": "0.35.3", "function.prototype.name": "1.0.3", "object.entries": "1.0.4", @@ -516,9 +516,9 @@ } }, "ajv": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.1.tgz", - "integrity": "sha1-s4u4h22ehr7plJVqBOch6IskjrI=", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "dev": true, "requires": { "co": "4.6.0", @@ -2493,7 +2493,7 @@ "dev": true, "requires": { "caniuse-lite": "1.0.30000783", - "electron-to-chromium": "1.3.28" + "electron-to-chromium": "1.3.29" } }, "bser": { @@ -2615,7 +2615,7 @@ "dev": true, "requires": { "caniuse-db": "1.0.30000783", - "electron-to-chromium": "1.3.28" + "electron-to-chromium": "1.3.29" } } } @@ -3505,7 +3505,7 @@ "dev": true, "requires": { "caniuse-db": "1.0.30000783", - "electron-to-chromium": "1.3.28" + "electron-to-chromium": "1.3.29" } }, "chalk": { @@ -3895,9 +3895,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.28", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.28.tgz", - "integrity": "sha1-jdTmRYCGZE6fnwoc8y4qH53/2e4=", + "version": "1.3.29", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.29.tgz", + "integrity": "sha1-elgja5VGjD52YAkTSFItZddzazY=", "dev": true }, "elliptic": { @@ -4000,9 +4000,9 @@ } }, "es5-shim": { - "version": "4.5.9", - "resolved": "https://registry.npmjs.org/es5-shim/-/es5-shim-4.5.9.tgz", - "integrity": "sha1-Kh4rnlg/9f7Qwgo+4svz91IwpcA=", + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/es5-shim/-/es5-shim-4.5.10.tgz", + "integrity": "sha512-vmryBdqKRO8Ei9LJ4yyEk/EOmAOGIagcHDYPpTAi6pot4IMHS1AC2q5cTKPmydpijg2iX8DVmCuqgrNxIWj8Yg==", "dev": true }, "es6-iterator": { @@ -4145,7 +4145,7 @@ "ignore": "3.3.7", "imurmurhash": "0.1.4", "inquirer": "0.12.0", - "is-my-json-valid": "2.16.1", + "is-my-json-valid": "2.17.1", "is-resolvable": "1.0.1", "js-yaml": "3.7.0", "json-stable-stringify": "1.0.1", @@ -4375,9 +4375,9 @@ } }, "eslint-plugin-prettier": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.3.1.tgz", - "integrity": "sha512-AV8shBlGN9tRZffj5v/f4uiQWlP3qiQ+lh+BhTqRLuKSyczx+HRWVkVZaf7dOmguxghAH1wftnou/JUEEChhGg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.4.0.tgz", + "integrity": "sha512-P0EohHM1MwL36GX5l1TOEYyt/5d7hcxrX3CqCjibTN3dH7VCAy2kjsC/WB6dUHnpB4mFkZq1Ndfh2DYQ2QMEGQ==", "dev": true, "requires": { "fast-diff": "1.1.2", @@ -5739,14 +5739,6 @@ } } }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, "string-width": { "version": "1.0.2", "bundled": true, @@ -5757,6 +5749,14 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, "stringstream": { "version": "0.0.5", "bundled": true, @@ -6232,7 +6232,7 @@ "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "dev": true, "requires": { - "ajv": "5.5.1", + "ajv": "5.5.2", "har-schema": "2.0.0" } }, @@ -6762,9 +6762,9 @@ } }, "is-my-json-valid": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz", - "integrity": "sha512-ochPsqWS1WXj8ZnMIV0vnNXooaMhp7cyL4FMSIPKTtnV0Ha/T19G2b9kkhcNsabV9bxYkze7/aLZJb/bYuFduQ==", + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz", + "integrity": "sha512-Q2khNw+oBlWuaYvEEHtKSw/pCxD2L5Rc1C+UQme9X6JdRDh7m5D7HkozA0qa3DUkQ6VzCnEm8mVIQPyIRkI5sQ==", "dev": true, "requires": { "generate-function": "2.0.0", @@ -8726,9 +8726,9 @@ } }, "moment": { - "version": "2.19.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.4.tgz", - "integrity": "sha512-1xFTAknSLfc47DIxHDUbnJWC+UwgWxATmymaxIPQpmMh7LBm7ZbwVEsuushqwL2GYZU0jie4xO+TK44hJPjNSQ==", + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==", "dev": true }, "ms": { @@ -8994,7 +8994,7 @@ "requires": { "chalk": "1.1.3", "commander": "2.12.2", - "is-my-json-valid": "2.16.1", + "is-my-json-valid": "2.17.1", "pinkie-promise": "2.0.1" } }, @@ -9604,9 +9604,9 @@ } }, "patternfly": { - "version": "3.31.2", - "resolved": "https://registry.npmjs.org/patternfly/-/patternfly-3.31.2.tgz", - "integrity": "sha1-XqvBVjE/2plQhSuil8vnPdp/U2M=", + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/patternfly/-/patternfly-3.32.1.tgz", + "integrity": "sha1-M3JdZSrJprf00/GKQYnbwxntWTo=", "requires": { "bootstrap": "3.3.7", "bootstrap-datepicker": "1.6.4", @@ -10780,7 +10780,7 @@ "dev": true, "requires": { "caniuse-db": "1.0.30000783", - "electron-to-chromium": "1.3.28" + "electron-to-chromium": "1.3.29" } }, "chalk": { @@ -12332,7 +12332,7 @@ "dev": true, "requires": { "@types/inline-style-prefixer": "3.0.1", - "@types/react": "16.0.30", + "@types/react": "16.0.31", "inline-style-prefixer": "3.0.8", "prop-types": "15.6.0", "react-style-proptype": "3.1.0" @@ -13087,7 +13087,7 @@ "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", "dev": true, "requires": { - "ajv": "5.5.1" + "ajv": "5.5.2" } }, "scss-tokenizer": { @@ -13502,15 +13502,6 @@ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", "dev": true }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", @@ -13575,6 +13566,15 @@ "function-bind": "1.1.1" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", @@ -14017,7 +14017,7 @@ "requires": { "chalk": "1.1.3", "commander": "2.12.2", - "is-my-json-valid": "2.16.1", + "is-my-json-valid": "2.17.1", "pinkie-promise": "2.0.1" } }, @@ -14545,7 +14545,7 @@ "requires": { "acorn": "5.2.1", "acorn-dynamic-import": "2.0.2", - "ajv": "5.5.1", + "ajv": "5.5.2", "ajv-keywords": "2.1.1", "async": "2.6.0", "enhanced-resolve": "3.4.1", diff --git a/sass/patternfly-react/_filter.scss b/sass/patternfly-react/_filter.scss new file mode 100644 index 00000000000..66fe70689df --- /dev/null +++ b/sass/patternfly-react/_filter.scss @@ -0,0 +1,36 @@ +.filter-pf { + .filter-pf-category-select { + display: flex; + } + .filter-pf-category-select-value { + border-left-width: 0; + } + + .filter-pf-category-item { + margin-bottom: 5px; + } + + .filter-pf-fields .form-group { + width: 275px; + } + .filter-pf-select-dropdown { + background-color: $color-pf-white; + background-image: none; + color: $color-pf-black-500; + font-size: 12px; + font-style: italic; + font-weight: 400; + padding-right: 25px; + text-align: left; + width: 100%; + z-index: 1; + } + .filter-pf-category-label { + font-weight: 700; + margin-right: 5px; + padding: 5px 0 6px 5px; + } + .filter-pf-select > .dropdown.btn-group { + width: 100%; + } +} diff --git a/sass/patternfly-react/_patternfly-react.scss b/sass/patternfly-react/_patternfly-react.scss index 91f53ee3a07..889608fc06c 100644 --- a/sass/patternfly-react/_patternfly-react.scss +++ b/sass/patternfly-react/_patternfly-react.scss @@ -2,3 +2,5 @@ /** Patternfly React Partials */ + +@import 'filter'; diff --git a/src/components/Filter/Filter.js b/src/components/Filter/Filter.js new file mode 100644 index 00000000000..7278c8c1990 --- /dev/null +++ b/src/components/Filter/Filter.js @@ -0,0 +1,389 @@ +import cx from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { DropdownButton } from 'react-bootstrap'; +import { MenuItem } from '../MenuItem'; +import { bindMethods } from '../../common/helpers'; + +class Filter extends React.Component { + constructor() { + super(); + this.state = { + currentField: null, + currentValue: '', + filterCategory: null, + filterValue: null, + }; + bindMethods(this, [ + 'selectField', + 'selectValue', + 'onValueKeyPress', + 'updateCurrentValue', + ]); + } + + setDefaultField(props) { + const { currentField } = this.state; + + if (props.fields && props.fields.length && !currentField) { + this.setState({ + currentField: props.fields[0], + }); + } + } + + componentWillMount() { + this.setDefaultField(this.props); + } + + componentWillReceiveProps(nextProps) { + this.setDefaultField(nextProps); + } + + selectField(field) { + const { currentField } = this.state; + + if (field === currentField) { + return; + } + + var updateState = { + currentField: field, + currentValue: '', + }; + + if (field.filterType === 'complex-select') { + updateState.filterCategory = field.filterCategories[0]; + } + + this.setState(updateState); + } + + selectValue(value, valueType) { + const { filterAddedCB } = this.props; + const { currentField, filterCategory } = this.state; + + if (value !== undefined) { + if (currentField.filterType === 'complex-select') { + switch (valueType) { + case 'filter-category': + this.setState({ filterCategory: value, filterValue: null }); + break; + case 'filter-value': + this.setState({ + filterValue: value, + }); + if (filterAddedCB) { + let filterValue = { + filterCategory: filterCategory, + filterValue: value, + }; + filterAddedCB(currentField, filterValue); + } + break; + } + } else { + this.setState({ currentValue: value }); + filterAddedCB && filterAddedCB(currentField, value); + } + } else { + switch (valueType) { + case 'filter-category': + this.setState({ filterCategory: '', filterValue: null }); + break; + case 'filter-value': + this.setState({ filterValue: '' }); + break; + } + } + } + + onValueKeyPress(keyEvent) { + const { filterAddedCB } = this.props; + const { currentValue, currentField } = this.state; + + if (keyEvent.key === 'Enter' && currentValue && currentValue.length > 0) { + this.setState({ currentValue: '' }); + filterAddedCB && filterAddedCB(currentField, currentValue); + keyEvent.stopPropagation(); + keyEvent.preventDefault(); + } + } + + updateCurrentValue(event) { + this.setState({ currentValue: event.target.value }); + } + + renderFieldMenuItems() { + const { fields } = this.props; + const { currentField } = this.state; + + return fields.map((item, index) => { + let classes = { + selected: item === currentField, + }; + return ( + this.selectField(item)} + > + {item.title} + + ); + }); + } + + renderFieldMenu() { + const { currentField } = this.state; + const { id, fields } = this.props; + if (!currentField) { + return null; + } + + let menuId = 'filterFieldMenu'; + menuId += id ? '_' + id : ''; + + if (fields && fields.length > 1) { + return ( +
+ + {this.renderFieldMenuItems()} + +
+ ); + } else { + return ( + + {currentField.label} + + ); + } + } + + renderPlaceHolder(placeholder, selectType) { + if (!placeholder) { + return null; + } + + return ( + this.selectValue(undefined, selectType)} + > + {placeholder} + + ); + } + + renderSelectMenuItems(filterValues, currentValue, selectType) { + if (!filterValues) { + return null; + } + + return filterValues.map((filterValue, index) => { + let classes = { + selected: filterValue === currentValue, + }; + + return ( + this.selectValue(filterValue, selectType)} + > + {filterValue.title || filterValue} + + ); + }); + } + + renderSelectMenu() { + const { currentField, currentValue } = this.state; + const { id } = this.props; + + let title; + if (!currentValue) { + title = currentField.placeholder; + } else if (currentValue.title) { + title = currentValue.title; + } else { + title = currentValue; + } + + let menuId = 'filterSelectMenu'; + menuId += id ? '_' + id : ''; + + return ( + + {this.renderPlaceHolder(currentField.placeholder)} + {this.renderSelectMenuItems( + currentField.filterValues, + currentValue, + '', + )} + + ); + } + + renderCategorySelectMenu() { + const { currentField, filterCategory } = this.state; + const { id } = this.props; + + let title; + if (!filterCategory) { + title = currentField.placeholder; + } else if (filterCategory.title) { + title = filterCategory.title; + } else { + title = filterCategory; + } + + let menuId = 'filterCategoryMenu'; + menuId += id ? '_' + id : ''; + + return ( +
+ + {this.renderPlaceHolder(currentField.placeholder, 'filter-category')} + {this.renderSelectMenuItems( + currentField.filterValues, + filterCategory, + 'filter-category', + )} + +
+ ); + } + + renderCategoryValueMenu() { + const { + id, + currentField, + currentValue, + filterCategory, + filterValue, + } = this.state; + + let title; + if (!filterValue) { + title = currentField.filterCategoriesPlaceholder; + } else if (filterValue.title) { + title = filterValue.title; + } else { + title = filterValue; + } + + let filterValues = null; + if (filterCategory) { + let category = null; + if (filterCategory.id) { + category = filterCategory.id.toLowerCase(); + } else { + category = filterCategory.toLowerCase(); + } + filterValues = currentField.filterCategories[category].filterValues; + } + + let menuId = 'filterCategoryValueMenu'; + menuId += id ? '_' + id : ''; + + return ( +
+ + {this.renderPlaceHolder( + currentField.filterCategoriesPlaceholder, + 'filter-value', + )} + {this.renderSelectMenuItems( + filterValues, + currentValue, + 'filter-value', + )} + +
+ ); + } + + renderInput() { + const { currentField, currentValue } = this.state; + if (!currentField) { + return null; + } + + if (currentField.filterType === 'select') { + return ( +
+ {this.renderSelectMenu()} +
+ ); + } else if (currentField.filterType === 'complex-select') { + return ( +
+ {this.renderCategorySelectMenu()} + {this.renderCategoryValueMenu()} +
+ ); + } else { + return ( + this.updateCurrentValue(e)} + onKeyPress={e => this.onValueKeyPress(e)} + /> + ); + } + } + + render() { + const { children, className } = this.props; + let classes = cx('filter-pf', className); + + return ( +
+
+
+ {this.renderFieldMenu()} + {this.renderInput()} +
+ {children} +
+
+ ); + } +} + +Filter.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, + /* ID for the filter component, necessary for accessibility if there are multiple filters on a page */ + id: PropTypes.string, // Necessary for accessibility if there are multiple fiters on a page + /* Array of FilterField objects */ + fields: PropTypes.array.isRequired, + /* function(field, value) - Callback to call when a filter is added */ + filterAddedCB: PropTypes.func, +}; + +export default Filter; diff --git a/src/components/Filter/Filter.stories.js b/src/components/Filter/Filter.stories.js new file mode 100644 index 00000000000..285282b2105 --- /dev/null +++ b/src/components/Filter/Filter.stories.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { defaultTemplate } from '../../../storybook/decorators/storyTemplates'; +import { Col, Row } from 'react-bootstrap'; +import { withInfo } from '@storybook/addon-info/dist/index'; +import { + MockFilterDisplay, + mockFilterDisplaySource, +} from './__mocks__/mockFilterDisplay'; +import { mockFilterFieldsSource } from './__mocks__/mockFilterFields'; + +const stories = storiesOf('Filter', module); + +const filterParameters = ` + id - (String) Necessary for accessibility if there are multiple filters on a page + filterFields - (Required) Array of filter fields for this filter (see definition below) + filterAddedCB - function(field, value), called when a filter is added +`; + +const filterFieldsType = ` + id - Optional unique Id for the filter field, useful for comparison + title - The label to display for the filter field + placeholder- Text to display when no filter value has been entered + filterType - The filter input field type (any html input type, or 'select' for a single select box or 'complex-select' for a category select box) + filterValues - List of valid select values used when filterType is 'select' or 'complex-select' (in where these values serve as case insensitve keys for .filterCategories objects) + filterCategories - For 'complex-select' only, array of objects whoes keys (case insensitive) match the .filterValues, these objects include each of the filter fields above (sans .placeholder) + filterCategoriesPlaceholder - Text to display in \`complex-select\` category value select when no filter value has been entered, Optional + filterAddedCB - Callback function when a filter is added (ctrl.currentField, ctrl.currentValue); +`; + +stories.addDecorator( + defaultTemplate({ + title: 'Filter', + documentationLink: + 'http://www.patternfly.org/pattern-library/forms-and-controls/filter/', + }), +); + +stories.add( + 'Filter', + withInfo({ + source: false, + propTablesExclude: [Row, Col, MockFilterDisplay], + text: ( +
+

Story Source

+

Filter Display

+
{mockFilterDisplaySource}
+

Filter Parameters

+
{filterParameters}
+

Filter Fields Definition

+
{filterFieldsType}
+

Example Filter Fields

+
{mockFilterFieldsSource}
+
+ ), + })(() => ( + + + + + + )), +); diff --git a/src/components/Filter/Filter.test.js b/src/components/Filter/Filter.test.js new file mode 100644 index 00000000000..10df2e0491b --- /dev/null +++ b/src/components/Filter/Filter.test.js @@ -0,0 +1,11 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { Filter } from '../../index'; +import { mockFilterFields } from './__mocks__/mockFilterFields'; + +test('Filter renders properly', () => { + const component = renderer.create(); + + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); diff --git a/src/components/Filter/__mocks__/mockFilterDisplay.js b/src/components/Filter/__mocks__/mockFilterDisplay.js new file mode 100644 index 00000000000..087116582da --- /dev/null +++ b/src/components/Filter/__mocks__/mockFilterDisplay.js @@ -0,0 +1,68 @@ +import React from 'react'; +import { Filter } from '../index'; +import { mockFilterFields } from './mockFilterFields'; + +export class MockFilterDisplay extends React.Component { + constructor() { + super(); + this.state = { + filtersText: '', + }; + } + + filterAdded = (field, value) => { + let filterText = ''; + if (field.title) { + filterText = field.title; + } else { + filterText = field; + } + filterText += ': '; + + if (value.filterCategory) { + filterText += + (value.filterCategory.title || value.filterCategory) + + '-' + + (value.filterValue.title || value.filterValue); + } else if (value.title) { + filterText += value.title; + } else { + filterText += value; + } + filterText += '\n'; + this.setState({ filtersText: this.state.filtersText + filterText }); + }; + + render() { + return ( +
+
+ +
+
+
+
+
+
+
+ +
+
+