From 59d62a9ed98ab4a6e933aafe4f999f6b790001f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2019 12:51:05 +0100 Subject: [PATCH 1/6] Update dependency webpack to v4.39.0 (#794) --- package-lock.json | 298 +++++++++++++++++++++++++++++++++++++--------- package.json | 2 +- 2 files changed, 245 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index c07477e8065..b83e01bbbfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7206,9 +7206,9 @@ "dev": true }, "cacache": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz", - "integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.2.tgz", + "integrity": "sha512-ifKgxH2CKhJEg6tNdAwziu6Q33EvuG26tYcda6PT3WKisZcYDXsnEdnRv67Po3yCzFfaSoMjGZzJyD2c3DT1dg==", "dev": true, "requires": { "bluebird": "^3.5.5", @@ -7216,6 +7216,7 @@ "figgy-pudding": "^3.5.1", "glob": "^7.1.4", "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", "lru-cache": "^5.1.1", "mississippi": "^3.0.0", "mkdirp": "^0.5.1", @@ -12934,6 +12935,12 @@ "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", "dev": true }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -20748,9 +20755,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -20760,60 +20767,116 @@ } }, "terser-webpack-plugin": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.3.0.tgz", - "integrity": "sha512-W2YWmxPjjkUcOWa4pBEv4OP4er1aeQJlSo2UhtCFQCuRXEHjOFscO8VyWHj9JLlA0RzQb8Y2/Ta78XZvT54uGg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz", + "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==", "dev": true, "requires": { - "cacache": "^11.3.2", - "find-cache-dir": "^2.0.0", + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", - "loader-utils": "^1.2.3", "schema-utils": "^1.0.0", "serialize-javascript": "^1.7.0", "source-map": "^0.6.1", - "terser": "^4.0.0", - "webpack-sources": "^1.3.0", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", "worker-farm": "^1.7.0" }, "dependencies": { - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { - "minimist": "^1.2.0" + "locate-path": "^3.0.0" } }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true + }, + "webpack-sources": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.1.tgz", + "integrity": "sha512-XSz38193PTo/1csJabKaV4b53uRVotlMgqJXm3s3eje0Bu6gQTxYDqpD38CmQfDBA+gN+QqaGjasuC8I/7eW3Q==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } } } }, @@ -22084,34 +22147,34 @@ "dev": true }, "webpack": { - "version": "4.38.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.38.0.tgz", - "integrity": "sha512-lbuFsVOq8PZY+1Ytz/mYOvYOo+d4IJ31hHk/7iyoeWtwN33V+5HYotSH+UIb9tq914ey0Hot7z6HugD+je3sWw==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.39.0.tgz", + "integrity": "sha512-nrxFNSEKm4T1C/EsgOgN50skt//Pl4X7kgJC1MrlE47M292LSCVmMOC47iTGL0CGxbdwhKGgeThrJcw0bstEfA==", "dev": true, "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-module-context": "1.8.5", "@webassemblyjs/wasm-edit": "1.8.5", "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.2.0", - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0", - "chrome-trace-event": "^1.0.0", + "acorn": "^6.2.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^4.1.0", - "eslint-scope": "^4.0.0", + "eslint-scope": "^4.0.3", "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.3.0", - "loader-utils": "^1.1.0", - "memory-fs": "~0.4.1", - "micromatch": "^3.1.8", - "mkdirp": "~0.5.0", - "neo-async": "^2.5.0", - "node-libs-browser": "^2.0.0", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", "schema-utils": "^1.0.0", - "tapable": "^1.1.0", - "terser-webpack-plugin": "^1.1.0", - "watchpack": "^1.5.0", - "webpack-sources": "^1.3.0" + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.1", + "watchpack": "^1.6.0", + "webpack-sources": "^1.4.1" }, "dependencies": { "acorn": { @@ -22119,6 +22182,133 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz", "integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==", "dev": true + }, + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + } + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + }, + "webpack-sources": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.1.tgz", + "integrity": "sha512-XSz38193PTo/1csJabKaV4b53uRVotlMgqJXm3s3eje0Bu6gQTxYDqpD38CmQfDBA+gN+QqaGjasuC8I/7eW3Q==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } } } }, diff --git a/package.json b/package.json index 0d9b9473d5d..27139795040 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "style-loader": "0.23.1", "stylelint": "10.1.0", "stylelint-config-wordpress": "14.0.0", - "webpack": "4.38.0", + "webpack": "4.39.0", "webpack-cli": "3.3.6", "yargs": "13.3.0" }, From e6072ba2f6621160a7607dd21ec3778617ca7399 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2019 12:51:52 +0100 Subject: [PATCH 2/6] Update Node.js to v10.16.1 (#789) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27139795040..fcbbe31b143 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "yargs": "13.3.0" }, "engines": { - "node": "10.16.0", + "node": "10.16.1", "npm": "6.10.2" }, "dependencies": { From b46d648dc10f1b5583caa11d6d90a865c44117df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Fri, 2 Aug 2019 13:56:53 +0200 Subject: [PATCH 3/6] Create `withProduct` HOC (#779) * Reset 'loaded' state in Featured Product and Category blocks * Minor code improvements * Remove state from ApiErrorPlaceholder * Move getProduct to a HOC * Undo changes in Featured Category * Update proptypes * Move error messages to * Reorder props * Move lifecycle methods to HOC * Make component presentational * Move withProduct to js/hocs * Create NAMESPACE constant in utils * Create getProduct util function * Set 'error' and 'product' state to null instead of 'false' when they have no value * Prevent getImageSrcFromProduct and getImageIdFromProduct returning an error when 'product' is not an object * Add HOC tests * Use 'toBe' and 'toBeNull' jest expect methods instead of 'toEqual' when possible * Export HOCs from index * Move mock implementation to beforeEach --- assets/js/blocks/featured-product/block.js | 346 ++++++------------ assets/js/blocks/featured-product/utils.js | 42 +++ .../components/api-error-placeholder/index.js | 100 ++--- assets/js/components/utils/index.js | 21 +- assets/js/hocs/index.js | 1 + assets/js/hocs/test/with-product.js | 103 ++++++ assets/js/hocs/with-product.js | 76 ++++ assets/js/utils/products.js | 24 +- 8 files changed, 430 insertions(+), 283 deletions(-) create mode 100644 assets/js/blocks/featured-product/utils.js create mode 100644 assets/js/hocs/index.js create mode 100644 assets/js/hocs/test/with-product.js create mode 100644 assets/js/hocs/with-product.js diff --git a/assets/js/blocks/featured-product/block.js b/assets/js/blocks/featured-product/block.js index a6b4e53ebbd..0940adc9c42 100644 --- a/assets/js/blocks/featured-product/block.js +++ b/assets/js/blocks/featured-product/block.js @@ -2,8 +2,6 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { escapeHTML } from '@wordpress/escape-html'; -import apiFetch from '@wordpress/api-fetch'; import { AlignmentToolbar, BlockControls, @@ -28,9 +26,9 @@ import { withSpokenMessages, } from '@wordpress/components'; import classnames from 'classnames'; -import { Component, Fragment } from '@wordpress/element'; +import { Fragment } from '@wordpress/element'; import { compose } from '@wordpress/compose'; -import { debounce, isObject, isEmpty } from 'lodash'; +import { isEmpty } from 'lodash'; import PropTypes from 'prop-types'; /** @@ -38,168 +36,35 @@ import PropTypes from 'prop-types'; */ import ProductControl from '../../components/product-control'; import ApiErrorPlaceholder from '../../components/api-error-placeholder'; +import { + dimRatioToClass, + getBackgroundImageStyles, +} from './utils'; import { getImageSrcFromProduct, getImageIdFromProduct, } from '../../utils/products'; +import { withProduct } from '../../hocs'; /** * The min-height for the block content. */ const MIN_HEIGHT = wc_product_block_data.min_height; -/** - * Generate a style object given either a product object or URL to an image. - * - * @param {object|string} url A product object as returned from the API, or an image URL. - * @return {object} A style object with a backgroundImage set (if a valid image is provided). - */ -function backgroundImageStyles( url ) { - // If `url` is an object, it's actually a product. - if ( isObject( url ) ) { - url = getImageSrcFromProduct( url ); - } - if ( url ) { - return { backgroundImage: `url(${ url })` }; - } - return {}; -} - -/** - * Convert the selected ratio to the correct background class. - * - * @param {number} ratio Selected opacity from 0 to 100. - * @return {string} The class name, if applicable (not used for ratio 0 or 50). - */ -function dimRatioToClass( ratio ) { - return ratio === 0 || ratio === 50 ? - null : - `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`; -} - /** * Component to handle edit mode of "Featured Product". */ -class FeaturedProduct extends Component { - constructor() { - super( ...arguments ); - this.state = { - product: false, - loaded: false, - error: false, - }; - - this.debouncedGetProduct = debounce( this.getProduct.bind( this ), 200 ); - } - - componentDidMount() { - this.getProduct(); - } - - componentWillUnmount() { - this.debouncedGetProduct.cancel(); - } - - componentDidUpdate( prevProps ) { - if ( prevProps.attributes.productId !== this.props.attributes.productId ) { - this.debouncedGetProduct(); - } - } - - getProduct() { - const { productId } = this.props.attributes; - if ( ! productId ) { - // We've removed the selected product, or no product is selected yet. - this.setState( { product: false, loaded: true, error: false } ); - return; - } - apiFetch( { - path: `/wc/blocks/products/${ productId }`, - } ) - .then( ( product ) => { - this.setState( { product, loaded: true, error: false } ); - } ) - .catch( ( apiError ) => { - const error = { - retry: this.debouncedGetProduct, - }; - - if ( isObject( apiError ) ) { - error.message = ( - - { __( 'The following error was returned from the API', 'woo-gutenberg-products-block' ) } -
- { escapeHTML( apiError.message ) } -
- ); - } else { - error.message = __( 'An unknown error occurred which prevented the block from being updated.', 'woo-gutenberg-products-block' ); - } - - this.setState( { error: false } ); // Force update if error stays same. - this.setState( { product: false, loaded: true, error: error } ); - } ); - } - - getInspectorControls() { - const { - attributes, - setAttributes, - overlayColor, - setOverlayColor, - } = this.props; - - const url = - attributes.mediaSrc || getImageSrcFromProduct( this.state.product ); - const { focalPoint = { x: 0.5, y: 0.5 } } = attributes; - - return ( - - - setAttributes( { showDesc: ! attributes.showDesc } ) } - /> - setAttributes( { showPrice: ! attributes.showPrice } ) } - /> - - - setAttributes( { dimRatio: ratio } ) } - min={ 0 } - max={ 100 } - step={ 10 } - /> - { !! FocalPointPicker && !! url && - setAttributes( { focalPoint: value } ) } - /> - } - - - ); - } - - renderEditMode() { - const { attributes, debouncedSpeak, setAttributes } = this.props; +const FeaturedProduct = ( { attributes, debouncedSpeak, error, getProduct, isLoading, isSelected, overlayColor, product, setAttributes, setOverlayColor } ) => { + const renderApiError = () => ( + + ); + + const renderEditMode = () => { const onDone = () => { setAttributes( { editMode: false } ); debouncedSpeak( @@ -212,7 +77,7 @@ class FeaturedProduct extends Component { return ( - { this.getBlockControls() } + { getBlockControls() } ); - } - - renderApiError() { - const { error } = this.state; - const onRetryCallback = () => { - error.retry(); - }; - return ( - - ); - } + }; - getBlockControls() { - const { attributes, setAttributes } = this.props; - const { product } = this.state; + const getBlockControls = () => { const { contentAlign, editMode } = attributes; const mediaId = attributes.mediaId || getImageIdFromProduct( product ); @@ -281,7 +130,7 @@ class FeaturedProduct extends Component { label={ __( 'Edit media' ) } icon="format-image" onClick={ open } - disabled={ ! this.state.product } + disabled={ ! product } /> ) } /> @@ -299,11 +148,58 @@ class FeaturedProduct extends Component { /> ); - } + }; + + const getInspectorControls = () => { + const url = attributes.mediaSrc || getImageSrcFromProduct( product ); + const { focalPoint = { x: 0.5, y: 0.5 } } = attributes; + + return ( + + + setAttributes( { showDesc: ! attributes.showDesc } ) } + /> + setAttributes( { showPrice: ! attributes.showPrice } ) } + /> + + + setAttributes( { dimRatio: ratio } ) } + min={ 0 } + max={ 100 } + step={ 10 } + /> + { !! FocalPointPicker && !! url && + setAttributes( { focalPoint: value } ) } + /> + } + + + ); + }; - renderProduct() { - const { attributes, isSelected, overlayColor, setAttributes } = this.props; - const { loaded, product } = this.state; + const renderProduct = () => { const { className, contentAlign, @@ -317,8 +213,8 @@ class FeaturedProduct extends Component { 'wc-block-featured-product', { 'is-selected': isSelected, - 'is-loading': ! product && ! loaded, - 'is-not-found': ! product && loaded, + 'is-loading': ! product && isLoading, + 'is-not-found': ! product && ! isLoading, 'has-background-dim': dimRatio !== 0, }, dimRatioToClass( dimRatio ), @@ -326,7 +222,7 @@ class FeaturedProduct extends Component { className, ); - const style = backgroundImageStyles( attributes.mediaSrc || product ); + const style = getBackgroundImageStyles( attributes.mediaSrc || product ); if ( overlayColor.color ) { style.backgroundColor = overlayColor.color; @@ -399,54 +295,44 @@ class FeaturedProduct extends Component { ); + }; + + const renderNoProduct = () => ( + + { isLoading ? ( + + ) : ( + __( 'No product is selected.', 'woo-gutenberg-products-block' ) + ) } + + ); + + const { editMode } = attributes; + + if ( error ) { + return renderApiError(); } - renderNoProduct() { - const { loaded } = this.state; - return ( - - { ! loaded ? ( - - ) : ( - __( 'No product is selected.', 'woo-gutenberg-products-block' ) - ) } - - ); + if ( editMode ) { + return renderEditMode(); } - render() { - const { product, error } = this.state; - const { attributes } = this.props; - const { editMode } = attributes; - - // If there was an API error, render it. - if ( error ) { - return this.renderApiError(); - } - - // If editing, show edit controls. - if ( editMode ) { - return this.renderEditMode(); - } - - // Otherwise render the selected product! - return ( - - { this.getBlockControls() } - { this.getInspectorControls() } - { !! product ? ( - this.renderProduct() - ) : ( - this.renderNoProduct() - ) } - - ); - } -} + return ( + + { getBlockControls() } + { getInspectorControls() } + { product ? ( + renderProduct() + ) : ( + renderNoProduct() + ) } + + ); +}; FeaturedProduct.propTypes = { /** @@ -465,6 +351,17 @@ FeaturedProduct.propTypes = { * A callback to update attributes. */ setAttributes: PropTypes.func.isRequired, + // from withProduct + error: PropTypes.object, + getProduct: PropTypes.func, + isLoading: PropTypes.bool, + product: PropTypes.shape( { + name: PropTypes.node, + variation: PropTypes.node, + description: PropTypes.node, + price_html: PropTypes.node, + permalink: PropTypes.string, + } ), // from withColors overlayColor: PropTypes.object, setOverlayColor: PropTypes.func.isRequired, @@ -473,6 +370,7 @@ FeaturedProduct.propTypes = { }; export default compose( [ + withProduct, withColors( { overlayColor: 'background-color' } ), withSpokenMessages, ] )( FeaturedProduct ); diff --git a/assets/js/blocks/featured-product/utils.js b/assets/js/blocks/featured-product/utils.js new file mode 100644 index 00000000000..eb7f1f80116 --- /dev/null +++ b/assets/js/blocks/featured-product/utils.js @@ -0,0 +1,42 @@ +/** + * External dependencies + */ +import { isObject } from 'lodash'; + +/** + * Internal dependencies + */ +import { + getImageSrcFromProduct, +} from '../../utils/products'; + +/** + * Generate a style object given either a product object or URL to an image. + * + * @param {object|string} url A product object as returned from the API, or an image URL. + * @return {object} A style object with a backgroundImage set (if a valid image is provided). + */ +function getBackgroundImageStyles( url ) { + // If `url` is an object, it's actually a product. + if ( isObject( url ) ) { + url = getImageSrcFromProduct( url ); + } + if ( url ) { + return { backgroundImage: `url(${ url })` }; + } + return {}; +} + +/** + * Convert the selected ratio to the correct background class. + * + * @param {number} ratio Selected opacity from 0 to 100. + * @return {string} The class name, if applicable (not used for ratio 0 or 50). + */ +function dimRatioToClass( ratio ) { + return ratio === 0 || ratio === 50 ? + null : + `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`; +} + +export { getBackgroundImageStyles, dimRatioToClass }; diff --git a/assets/js/components/api-error-placeholder/index.js b/assets/js/components/api-error-placeholder/index.js index d18fb534d3e..9596eeab191 100644 --- a/assets/js/components/api-error-placeholder/index.js +++ b/assets/js/components/api-error-placeholder/index.js @@ -2,75 +2,85 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { Component, Fragment } from '@wordpress/element'; +import { Fragment } from '@wordpress/element'; import PropTypes from 'prop-types'; import Gridicon from 'gridicons'; import classNames from 'classnames'; +import { escapeHTML } from '@wordpress/escape-html'; import { Button, Placeholder, Spinner, } from '@wordpress/components'; -/** - * Internal dependencies - */ - -class ApiErrorPlaceholder extends Component { - constructor() { - super( ...arguments ); - this.state = { - retrying: false, - }; - this.onRetry = this.onRetry.bind( this ); +const getErrorMessage = ( { apiMessage, message } ) => { + if ( message ) { + return message; } - onRetry() { - const { onRetry } = this.props; - - this.setState( { retrying: true } ); - onRetry(); - } - - render() { - const { onRetry, errorMessage, className } = this.props; - const { retrying } = this.state; + if ( apiMessage ) { return ( - } - label={ __( 'Sorry, an error occurred', 'woo-gutenberg-products-block' ) } - className={ classNames( 'wc-block-api-error', className ) } - > -
{ errorMessage }
- { onRetry && ( - - { !! retrying ? ( - - ) : ( - - ) } - - ) } -
+ + { __( 'The following error was returned from the API', 'woo-gutenberg-products-block' ) } +
+ { escapeHTML( apiMessage ) } +
); } -} + + return __( 'An unknown error occurred which prevented the block from being updated.', 'woo-gutenberg-products-block' ); +}; + +const ApiErrorPlaceholder = ( { className, error, isLoading, onRetry } ) => ( + } + label={ __( 'Sorry, an error occurred', 'woo-gutenberg-products-block' ) } + className={ classNames( 'wc-block-api-error', className ) } + > +
+ { getErrorMessage( error ) } +
+ { onRetry && ( + + { isLoading ? ( + + ) : ( + + ) } + + ) } +
+); ApiErrorPlaceholder.propTypes = { /** * Callback to retry an action. */ onRetry: PropTypes.func.isRequired, - /** - * The error message to display from the API. - */ - errorMessage: PropTypes.node, /** * Classname to add to placeholder in addition to the defaults. */ className: PropTypes.string, + /** + * The error object. + */ + error: PropTypes.shape( { + /** + * API error message to display in case of a missing `message`. + */ + apiMessage: PropTypes.node, + /** + * Human-readable error message to display. + */ + message: PropTypes.string, + } ), + /** + * Whether there is a request running, so the 'Retry' button is hidden and + * a spinner is shown instead. + */ + isLoading: PropTypes.bool, }; export default ApiErrorPlaceholder; diff --git a/assets/js/components/utils/index.js b/assets/js/components/utils/index.js index 4088798056a..e720cd256c2 100644 --- a/assets/js/components/utils/index.js +++ b/assets/js/components/utils/index.js @@ -9,9 +9,11 @@ export const isLargeCatalog = wc_product_block_data.isLargeCatalog || false; export const limitTags = wc_product_block_data.limitTags || false; export const hasTags = wc_product_block_data.hasTags || false; +const NAMESPACE = '/wc/blocks/products'; + const getProductsRequests = ( { selected = [], search } ) => { const requests = [ - addQueryArgs( '/wc/blocks/products', { + addQueryArgs( NAMESPACE, { per_page: isLargeCatalog ? 100 : -1, catalog_visibility: 'visible', status: 'publish', @@ -22,7 +24,7 @@ const getProductsRequests = ( { selected = [], search } ) => { // If we have a large catalog, we might not get all selected products in the first page. if ( isLargeCatalog && selected.length ) { requests.push( - addQueryArgs( '/wc/blocks/products', { + addQueryArgs( NAMESPACE, { catalog_visibility: 'visible', status: 'publish', include: selected, @@ -46,9 +48,20 @@ export const getProducts = ( { selected = [], search } ) => { } ); }; +/** + * Get a promise that resolves to a product object from the API. + * + * @param {object} - Id of the product to retrieve. + */ +export const getProduct = ( productId ) => { + return apiFetch( { + path: `${ NAMESPACE }/${ productId }`, + } ); +}; + const getProductTagsRequests = ( { selected = [], search } ) => { const requests = [ - addQueryArgs( '/wc/blocks/products/tags', { + addQueryArgs( `${ NAMESPACE }/tags`, { per_page: limitTags ? 100 : -1, orderby: limitTags ? 'count' : 'name', order: limitTags ? 'desc' : 'asc', @@ -59,7 +72,7 @@ const getProductTagsRequests = ( { selected = [], search } ) => { // If we have a large catalog, we might not get all selected products in the first page. if ( limitTags && selected.length ) { requests.push( - addQueryArgs( '/wc/blocks/products/tags', { + addQueryArgs( `${ NAMESPACE }/tags`, { include: selected, } ) ); diff --git a/assets/js/hocs/index.js b/assets/js/hocs/index.js new file mode 100644 index 00000000000..496e4d92ce8 --- /dev/null +++ b/assets/js/hocs/index.js @@ -0,0 +1 @@ +export { default as withProduct } from './with-product'; diff --git a/assets/js/hocs/test/with-product.js b/assets/js/hocs/test/with-product.js new file mode 100644 index 00000000000..1df4b8c0c2c --- /dev/null +++ b/assets/js/hocs/test/with-product.js @@ -0,0 +1,103 @@ +/** + * External dependencies + */ +import TestRenderer from 'react-test-renderer'; + +/** + * Internal dependencies + */ +import withProduct from '../with-product'; +import * as mockUtils from '../../components/utils'; + +// Mock the getProduct functions for tests. +jest.mock( '../../components/utils', () => ( { + getProduct: jest.fn(), +} ) ); + +const mockProduct = { name: 'T-Shirt' }; +const attributes = { productId: 1 }; +const TestComponent = withProduct( ( props ) => { + return
; +} ); +const render = () => { + return TestRenderer.create( + + ); +}; + +describe( 'withProduct Component', () => { + let renderer; + afterEach( () => { + mockUtils.getProduct.mockReset(); + } ); + + describe( 'lifecycle events', () => { + beforeEach( () => { + mockUtils.getProduct.mockImplementation( () => Promise.resolve() ); + renderer = render(); + } ); + + describe( 'test', () => { + it( 'getProduct is called on mount with passed in product id', () => { + const { getProduct } = mockUtils; + + expect( getProduct ).toHaveBeenCalledWith( attributes.productId ); + expect( getProduct ).toHaveBeenCalledTimes( 1 ); + } ); + } ); + + describe( 'test', () => { + it( 'getProduct is hooked to the prop', () => { + const { getProduct } = mockUtils; + const props = renderer.root.findByType( 'div' ).props; + + props.getProduct(); + + expect( getProduct ).toHaveBeenCalledTimes( 2 ); + } ); + } ); + } ); + + describe( 'when the API returns product data', () => { + beforeEach( () => { + mockUtils.getProduct.mockImplementation( + ( productId ) => Promise.resolve( { ...mockProduct, id: productId } ) + ); + renderer = render(); + } ); + + it( 'sets the product props', () => { + const props = renderer.root.findByType( 'div' ).props; + + expect( props.error ).toBeNull(); + expect( typeof props.getProduct ).toBe( 'function' ); + expect( props.isLoading ).toBe( false ); + expect( props.product ).toEqual( { ...mockProduct, id: attributes.productId } ); + } ); + } ); + + describe( 'when the API returns an error', () => { + beforeEach( () => { + mockUtils.getProduct.mockImplementation( + () => Promise.reject( { message: 'There was an error.' } ) + ); + renderer = render(); + } ); + + it( 'sets the error prop', () => { + const props = renderer.root.findByType( 'div' ).props; + + expect( props.error ).toEqual( { apiMessage: 'There was an error.' } ); + expect( typeof props.getProduct ).toBe( 'function' ); + expect( props.isLoading ).toBe( false ); + expect( props.product ).toBeNull(); + } ); + } ); +} ); diff --git a/assets/js/hocs/with-product.js b/assets/js/hocs/with-product.js new file mode 100644 index 00000000000..72fdfc26877 --- /dev/null +++ b/assets/js/hocs/with-product.js @@ -0,0 +1,76 @@ +/** + * External dependencies + */ +import { Component } from '@wordpress/element'; +import { createHigherOrderComponent } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { getProduct } from '../components/utils'; + +const withProduct = createHigherOrderComponent( + ( OriginalComponent ) => { + return class WrappedComponent extends Component { + constructor() { + super( ...arguments ); + this.state = { + error: null, + loading: false, + product: null, + }; + this.loadProduct = this.loadProduct.bind( this ); + } + + componentDidMount() { + this.loadProduct(); + } + + componentDidUpdate( prevProps ) { + if ( prevProps.attributes.productId !== this.props.attributes.productId ) { + this.loadProduct(); + } + } + + loadProduct() { + const { productId } = this.props.attributes; + + if ( ! productId ) { + this.setState( { product: null, loading: false, error: null } ); + return; + } + + this.setState( { loading: true } ); + + getProduct( productId ).then( ( product ) => { + this.setState( { product, loading: false, error: null } ); + } ).catch( ( apiError ) => { + const error = typeof apiError === 'object' && apiError.hasOwnProperty( 'message' ) ? { + apiMessage: apiError.message, + } : { + // If we can't get any message from the API, set it to null and + // let handle the message to display. + apiMessage: null, + }; + + this.setState( { product: null, loading: false, error } ); + } ); + } + + render() { + const { error, loading, product } = this.state; + + return ; + } + }; + }, + 'withProduct' +); + +export default withProduct; diff --git a/assets/js/utils/products.js b/assets/js/utils/products.js index 3263b3c74e0..6b4acd69948 100644 --- a/assets/js/utils/products.js +++ b/assets/js/utils/products.js @@ -1,25 +1,29 @@ /** * Get the src of the first image attached to a product (the featured image). * - * @param {array} images The array of images, destructured from the product object. + * @param {object} product The product object to get the images from. + * @param {array} product.images The array of images, destructured from the product object. * @return {string} The full URL to the image. */ -export function getImageSrcFromProduct( { images = [] } ) { - if ( images.length ) { - return images[ 0 ].src || ''; +export function getImageSrcFromProduct( product ) { + if ( ! product || ! product.images || ! product.images.length ) { + return 0; } - return ''; + + return product.images[ 0 ].src || ''; } /** * Get the ID of the first image attached to a product (the featured image). * - * @param {array} images The array of images, destructured from the product object. + * @param {object} product The product object to get the images from. + * @param {array} product.images The array of images, destructured from the product object. * @return {number} The ID of the image. */ -export function getImageIdFromProduct( { images = [] } ) { - if ( images.length ) { - return images[ 0 ].id || 0; +export function getImageIdFromProduct( product ) { + if ( ! product || ! product.images || ! product.images.length ) { + return 0; } - return 0; + + return product.images[ 0 ].id || 0; } From d7404bc97da6c367070b831e3992cd79d8461112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 5 Aug 2019 11:37:16 +0200 Subject: [PATCH 4/6] Move withComponentId to hocs directory (#797) --- assets/js/blocks/product-categories/block.js | 2 +- assets/js/hocs/index.js | 1 + assets/js/{utils => hocs}/with-component-id.js | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename assets/js/{utils => hocs}/with-component-id.js (100%) diff --git a/assets/js/blocks/product-categories/block.js b/assets/js/blocks/product-categories/block.js index 83eebde0117..6beecd17adb 100644 --- a/assets/js/blocks/product-categories/block.js +++ b/assets/js/blocks/product-categories/block.js @@ -8,7 +8,7 @@ import classnames from 'classnames'; /** * Internal dependencies */ -import withComponentId from '../../utils/with-component-id'; +import { withComponentId } from '../../hocs'; /** * Component displaying the categories as dropdown or list. diff --git a/assets/js/hocs/index.js b/assets/js/hocs/index.js index 496e4d92ce8..4c58a82bb9e 100644 --- a/assets/js/hocs/index.js +++ b/assets/js/hocs/index.js @@ -1 +1,2 @@ +export { default as withComponentId } from './with-component-id'; export { default as withProduct } from './with-product'; diff --git a/assets/js/utils/with-component-id.js b/assets/js/hocs/with-component-id.js similarity index 100% rename from assets/js/utils/with-component-id.js rename to assets/js/hocs/with-component-id.js From 1d0ec5122b2bf167929e8936e7b556d8cca8f611 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2019 10:40:45 +0100 Subject: [PATCH 5/6] Update dependency webpack to v4.39.1 (#798) --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b83e01bbbfd..58e27b2528c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22147,9 +22147,9 @@ "dev": true }, "webpack": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.39.0.tgz", - "integrity": "sha512-nrxFNSEKm4T1C/EsgOgN50skt//Pl4X7kgJC1MrlE47M292LSCVmMOC47iTGL0CGxbdwhKGgeThrJcw0bstEfA==", + "version": "4.39.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.39.1.tgz", + "integrity": "sha512-/LAb2TJ2z+eVwisldp3dqTEoNhzp/TLCZlmZm3GGGAlnfIWDgOEE758j/9atklNLfRyhKbZTCOIoPqLJXeBLbQ==", "dev": true, "requires": { "@webassemblyjs/ast": "1.8.5", diff --git a/package.json b/package.json index fcbbe31b143..3acffecf645 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "style-loader": "0.23.1", "stylelint": "10.1.0", "stylelint-config-wordpress": "14.0.0", - "webpack": "4.39.0", + "webpack": "4.39.1", "webpack-cli": "3.3.6", "yargs": "13.3.0" }, From 1e37fa29aab71ab5a4b54bbc3c2ee9e36d95a9f2 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 5 Aug 2019 11:25:57 +0100 Subject: [PATCH 6/6] Add product search block (#697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * basic scaffold * Progress * Fix saving * Move data to form * Update assets/js/blocks/product-search/editor.scss Co-Authored-By: Albert Juhé Lluveras * Update assets/js/blocks/product-search/block.js Co-Authored-By: Albert Juhé Lluveras * hex case * Use a span element * Move render method * CSS * Update buttons * Fix navigation buttons * remove webkit appearance styles for buttons * Swap rich to plain text component * Improved attribute handling * Update assets/js/blocks/product-search/block.js Co-Authored-By: Albert Juhé Lluveras --- .../js/blocks/product-categories/style.scss | 1 - assets/js/blocks/product-search/block.js | 137 ++++++++++++++++++ assets/js/blocks/product-search/editor.scss | 10 ++ assets/js/blocks/product-search/index.js | 113 +++++++++++++++ assets/js/blocks/product-search/style.scss | 62 ++++++++ src/Assets.php | 1 + src/BlockTypes/ProductSearch.php | 23 +++ src/Library.php | 1 + webpack.config.js | 1 + 9 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 assets/js/blocks/product-search/block.js create mode 100644 assets/js/blocks/product-search/editor.scss create mode 100644 assets/js/blocks/product-search/index.js create mode 100644 assets/js/blocks/product-search/style.scss create mode 100644 src/BlockTypes/ProductSearch.php diff --git a/assets/js/blocks/product-categories/style.scss b/assets/js/blocks/product-categories/style.scss index 8edbcefbc12..410166f6d74 100644 --- a/assets/js/blocks/product-categories/style.scss +++ b/assets/js/blocks/product-categories/style.scss @@ -27,7 +27,6 @@ margin: 0; border: none; cursor: pointer; - -webkit-appearance: none; background: none; padding: 8px; color: #555d66; diff --git a/assets/js/blocks/product-search/block.js b/assets/js/blocks/product-search/block.js new file mode 100644 index 00000000000..9ff1ec1b3de --- /dev/null +++ b/assets/js/blocks/product-search/block.js @@ -0,0 +1,137 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import classnames from 'classnames'; +import { Component } from '@wordpress/element'; +import PropTypes from 'prop-types'; +import { withInstanceId, compose } from '@wordpress/compose'; +import { PlainText } from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import './editor.scss'; +import './style.scss'; + +/** + * Component displaying a product search form. + */ +class ProductSearchBlock extends Component { + renderView() { + const { attributes: { label, placeholder, formId, className, hasLabel, align } } = this.props; + const home = wc_product_block_data.homeUrl; + const classes = classnames( + 'wc-block-product-search', + align ? 'align' + align : '', + className, + ); + + return ( +
+
+ +
+ + + +
+
+
+ ); + } + + renderEdit() { + const { attributes, setAttributes, instanceId } = this.props; + const { label, placeholder, formId, className, hasLabel, align } = attributes; + const classes = classnames( + 'wc-block-product-search', + align ? 'align' + align : '', + className, + ); + + if ( ! formId ) { + setAttributes( { formId: `wc-block-product-search-${ instanceId }` } ); + } + + return ( +
+ { !! hasLabel && ( + setAttributes( { label: value } ) } + /> + ) } + <div className="wc-block-product-search__fields"> + <PlainText + className="wc-block-product-search__field input-control" + value={ placeholder } + onChange={ ( value ) => setAttributes( { placeholder: value } ) } + /> + <button + type="submit" + className="wc-block-product-search__button" + label={ __( 'Search', 'woo-gutenberg-products-block' ) } + onClick={ ( e ) => e.preventDefault() } + tabindex="-1" + > + <svg aria-hidden="true" role="img" focusable="false" className="dashicon dashicons-arrow-right-alt2" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"> + <path d="M6 15l5-5-5-5 1-2 7 7-7 7z"></path> + </svg> + </button> + </div> + </div> + ); + } + + render() { + if ( this.props.isPreview ) { + return this.renderEdit(); + } + + return this.renderView(); + } +} + +ProductSearchBlock.propTypes = { + /** + * The attributes for this block. + */ + attributes: PropTypes.object.isRequired, + /** + * A unique ID for identifying the label for the select dropdown. + */ + instanceId: PropTypes.number, + /** + * Whether this is the block preview or frontend display. + */ + isPreview: PropTypes.bool, + /** + * A callback to update attributes. + */ + setAttributes: PropTypes.func, +}; + +export default compose( [ + withInstanceId, +] )( ProductSearchBlock ); diff --git a/assets/js/blocks/product-search/editor.scss b/assets/js/blocks/product-search/editor.scss new file mode 100644 index 00000000000..2f50991f081 --- /dev/null +++ b/assets/js/blocks/product-search/editor.scss @@ -0,0 +1,10 @@ +.wc-block-product-search__field.input-control { + color: #828b96 !important; +} +.wc-block-product-search { + .wc-block-product-search__fields { + .block-editor-rich-text { + flex-grow: 1; + } + } +} diff --git a/assets/js/blocks/product-search/index.js b/assets/js/blocks/product-search/index.js new file mode 100644 index 00000000000..c2cebc8405d --- /dev/null +++ b/assets/js/blocks/product-search/index.js @@ -0,0 +1,113 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { registerBlockType } from '@wordpress/blocks'; +import { InspectorControls } from '@wordpress/editor'; +import { PanelBody, ToggleControl } from '@wordpress/components'; +import { Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import './style.scss'; +import './editor.scss'; +import Block from './block.js'; + +registerBlockType( 'woocommerce/product-search', { + title: __( 'Product Search', 'woo-gutenberg-products-block' ), + icon: { + src: 'search', + foreground: '#96588a', + }, + category: 'woocommerce', + keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ], + description: __( + 'Help visitors find your products.', + 'woo-gutenberg-products-block' + ), + supports: { + align: [ 'wide', 'full' ], + }, + + attributes: { + /** + * Whether to show the field label. + */ + hasLabel: { + type: 'boolean', + default: true, + }, + + /** + * Search field label. + */ + label: { + type: 'string', + default: __( 'Search', 'woo-gutenberg-products-block' ), + source: 'text', + selector: 'label', + }, + + /** + * Search field placeholder. + */ + placeholder: { + type: 'string', + default: __( 'Search products...', 'woo-gutenberg-products-block' ), + source: 'attribute', + selector: 'input.wc-block-product-search__field', + attribute: 'placeholder', + }, + + /** + * Store the instance ID. + */ + formId: { + type: 'string', + default: '', + }, + }, + + /** + * Renders and manages the block. + */ + edit( props ) { + const { attributes, setAttributes } = props; + const { hasLabel } = attributes; + return ( + <Fragment> + <InspectorControls key="inspector"> + <PanelBody + title={ __( 'Content', 'woo-gutenberg-products-block' ) } + initialOpen + > + + <ToggleControl + label={ __( 'Show search field label', 'woo-gutenberg-products-block' ) } + help={ + hasLabel ? + __( 'Label is visible.', 'woo-gutenberg-products-block' ) : + __( 'Label is hidden.', 'woo-gutenberg-products-block' ) + } + checked={ hasLabel } + onChange={ () => setAttributes( { hasLabel: ! hasLabel } ) } + /> + </PanelBody> + </InspectorControls> + <Block { ...props } isPreview /> + </Fragment> + ); + }, + + /** + * Save the props to post content. + */ + save( attributes ) { + return ( + <div> + <Block { ...attributes } /> + </div> + ); + }, +} ); diff --git a/assets/js/blocks/product-search/style.scss b/assets/js/blocks/product-search/style.scss new file mode 100644 index 00000000000..c83da51683c --- /dev/null +++ b/assets/js/blocks/product-search/style.scss @@ -0,0 +1,62 @@ +.wc-block-product-search { + .wc-block-product-search__fields { + display: flex; + } + .wc-block-product-search__field { + padding: 6px 8px; + line-height: 1.8; + flex-grow: 1; + } + .wc-block-product-search__button { + display: flex; + align-items: center; + text-decoration: none; + font-size: 13px; + margin: 0 0 0 6px; + border: none; + cursor: pointer; + background: none; + padding: 8px; + color: #555d66; + position: relative; + overflow: hidden; + border-radius: 4px; + svg { + fill: currentColor; + outline: none; + } + .screen-reader-text { + height: auto; + } + &:active { + color: currentColor; + } + &:disabled, + &[aria-disabled="true"] { + cursor: default; + opacity: 0.3; + } + &:focus:enabled { + background-color: #fff; + color: #191e23; + box-shadow: inset 0 0 0 1px #6c7781, inset 0 0 0 2px #fff; + outline: 2px solid transparent; + outline-offset: -2px; + } + &:not(:disabled):not([aria-disabled="true"]):hover { + background-color: #fff; + color: #191e23; + box-shadow: inset 0 0 0 1px #e2e4e7, inset 0 0 0 2px #fff, 0 1px 1px rgba(25, 30, 35, 0.2); + } + &:not(:disabled):not([aria-disabled="true"]):active { + outline: none; + background-color: #fff; + color: #191e23; + box-shadow: inset 0 0 0 1px #ccd0d4, inset 0 0 0 2px #fff; + } + &[aria-disabled="true"]:focus, + &:disabled:focus { + box-shadow: none; + } + } +} diff --git a/src/Assets.php b/src/Assets.php index 5f5747f7e8d..a6382b715f2 100644 --- a/src/Assets.php +++ b/src/Assets.php @@ -48,6 +48,7 @@ public static function register_assets() { self::register_script( 'wc-featured-category', plugins_url( 'build/featured-category.js', __DIR__ ), array( 'wc-vendors', 'wc-blocks' ) ); self::register_script( 'wc-product-categories', plugins_url( 'build/product-categories.js', __DIR__ ), array( 'wc-vendors', 'wc-blocks' ) ); self::register_script( 'wc-product-tag', plugins_url( 'build/product-tag.js', __DIR__ ), array( 'wc-vendors', 'wc-blocks' ) ); + self::register_script( 'wc-product-search', plugins_url( 'build/product-search.js', __DIR__ ), array( 'wc-vendors', 'wc-blocks' ) ); } /** diff --git a/src/BlockTypes/ProductSearch.php b/src/BlockTypes/ProductSearch.php new file mode 100644 index 00000000000..6bab7c31d36 --- /dev/null +++ b/src/BlockTypes/ProductSearch.php @@ -0,0 +1,23 @@ +<?php +/** + * Product search block. + * + * @package WooCommerce/Blocks + */ + +namespace Automattic\WooCommerce\Blocks\BlockTypes; + +defined( 'ABSPATH' ) || exit; + +/** + * ProductSearch class. + */ +class ProductSearch extends AbstractBlock { + + /** + * Block name. + * + * @var string + */ + protected $block_name = 'product-search'; +} diff --git a/src/Library.php b/src/Library.php index 08e061fc3ed..ddd234a7a82 100644 --- a/src/Library.php +++ b/src/Library.php @@ -36,6 +36,7 @@ public static function register_blocks() { 'ProductOnSale', 'ProductsByAttribute', 'ProductTopRated', + 'ProductSearch', 'ProductTag', ]; foreach ( $blocks as $class ) { diff --git a/webpack.config.js b/webpack.config.js index 8bd91de9304..a638d741461 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -53,6 +53,7 @@ const GutenbergBlocksConfig = { 'product-top-rated': './assets/js/blocks/product-top-rated/index.js', 'products-by-attribute': './assets/js/blocks/products-by-attribute/index.js', 'featured-product': './assets/js/blocks/featured-product/index.js', + 'product-search': './assets/js/blocks/product-search/index.js', 'product-tag': './assets/js/blocks/product-tag/index.js', 'featured-category': './assets/js/blocks/featured-category/index.js', },