diff --git a/package-lock.json b/package-lock.json index ba5bc32..c7f1ca3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "final-form-arrays", - "version": "3.0.2", + "version": "3.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1454,15 +1454,23 @@ "dev": true }, "ajv": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.1.tgz", - "integrity": "sha512-w1YQaVGNC6t2UCPjEawK/vo/dG8OOrVtUmhBT1uJJYxbl5kU2Tj3v6LGqBcsysN1yhuCStJCCA3GqdvKY8sqXQ==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "dependencies": { + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + } } }, "anchor-markdown-header": { @@ -2008,14 +2016,36 @@ } }, "browserslist": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.4.tgz", - "integrity": "sha512-ErJT8qGfRt/VWHSr1HeqZzz50DvxHtr1fVL1m5wf20aGrG8e1ce8fpZ2EjZEfs09DDZYSvtRaDlMpWslBf8Low==", + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30000981", - "electron-to-chromium": "^1.3.188", - "node-releases": "^1.1.25" + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + }, + "dependencies": { + "caniuse-lite": { + "version": "1.0.30001228", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz", + "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.736", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz", + "integrity": "sha512-DY8dA7gR51MSo66DqitEQoUMQ0Z+A2DSXFi7tK304bdTVqczCAfUuyQw6Wdg8hIoo5zIxkU1L24RQtUce1Ioig==", + "dev": true + }, + "node-releases": { + "version": "1.1.72", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==", + "dev": true + } } }, "bser": { @@ -2151,12 +2181,6 @@ "quick-lru": "^1.0.0" } }, - "caniuse-lite": { - "version": "1.0.30000984", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000984.tgz", - "integrity": "sha512-n5tKOjMaZ1fksIpQbjERuqCyfgec/m9pferkFQbLmWtqLUdmt12hNhjSwsmPdqeiG2NkITOQhr1VYIwWSAceiA==", - "dev": true - }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -2402,6 +2426,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3110,9 +3140,9 @@ } }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true }, "decompress-response": { @@ -3334,12 +3364,6 @@ "safer-buffer": "^2.1.0" } }, - "electron-to-chromium": { - "version": "1.3.191", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.191.tgz", - "integrity": "sha512-jasjtY5RUy/TOyiUYM2fb4BDaPZfm6CXRFeJDMfFsXYADGxUN49RBqtgB7EL2RmJXeIRUk9lM1U6A5yk2YJMPQ==", - "dev": true - }, "elegant-spinner": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", @@ -3401,6 +3425,12 @@ "is-symbol": "^1.0.2" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -4115,12 +4145,6 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "dev": true }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -4199,12 +4223,29 @@ } }, "final-form": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.18.2.tgz", - "integrity": "sha512-VQx/5x9M4CiC8fG678Dm1IS3mXvBl7ZNIUx5tUZCk00lFImJzQix4KO0+eGtl49sha2bYOxuYn8jRJiq6sazXA==", + "version": "4.20.8", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.8.tgz", + "integrity": "sha512-knyoRbK42XvZOwfElVSZEfE9licdOmgLLUfPSuxJOl0UhjYT1HiBaHZlfYnwqmvo1H9ucNGyXDHoUP0vI0474g==", "dev": true, "requires": { - "@babel/runtime": "^7.3.1" + "@babel/runtime": "^7.10.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + } } }, "find-up": { @@ -4325,6 +4366,16 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.6.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4388,9 +4439,7 @@ }, "chownr": { "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true + "bundled": true }, "code-point-at": { "version": "1.1.0", @@ -4446,8 +4495,6 @@ "fs-minipass": { "version": "1.2.5", "bundled": true, - "dev": true, - "optional": true, "requires": { "minipass": "^2.2.1" } @@ -4567,7 +4614,6 @@ "minipass": { "version": "2.3.5", "bundled": true, - "dev": true, "optional": true, "requires": { "safe-buffer": "^5.1.2", @@ -4577,8 +4623,6 @@ "minizlib": { "version": "1.2.1", "bundled": true, - "dev": true, - "optional": true, "requires": { "minipass": "^2.2.1" } @@ -4767,7 +4811,6 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, "optional": true }, "safer-buffer": { @@ -4835,21 +4878,6 @@ "dev": true, "optional": true }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, "util-deprecate": { "version": "1.0.2", "bundled": true, @@ -4874,7 +4902,6 @@ "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, "optional": true } } @@ -5143,17 +5170,24 @@ } }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", "dev": true, "requires": { + "minimist": "^1.2.5", "neo-async": "^2.6.0", - "optimist": "^0.6.1", "source-map": "^0.6.1", - "uglify-js": "^3.1.4" + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" }, "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5247,9 +5281,9 @@ } }, "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "html-encoding-sniffer": { @@ -6526,12 +6560,6 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6551,24 +6579,29 @@ "dev": true }, "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true }, "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" + }, + "dependencies": { + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + } } }, "jsx-ast-utils": { @@ -6890,9 +6923,9 @@ } }, "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash.debounce": { @@ -7349,6 +7382,36 @@ "is-plain-obj": "^1.1.0" } }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "optional": true + } + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.9.0" + } + }, "mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -7509,15 +7572,6 @@ "which": "^1.3.0" } }, - "node-releases": { - "version": "1.1.25", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.25.tgz", - "integrity": "sha512-fI5BXuk83lKEoZDdH3gRhtsNgh05/wZacuXkgbiYkceE7+QIMXOg98n9ZV7mz27B+kFHnqHcUpscZZlGRSmTpQ==", - "dev": true, - "requires": { - "semver": "^5.3.0" - } - }, "noop-logger": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", @@ -7817,9 +7871,9 @@ } }, "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", "dev": true }, "yargs": { @@ -8234,30 +8288,6 @@ } } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } - }, "optionator": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", @@ -9286,9 +9316,9 @@ "dev": true }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "dev": true }, "quick-lru": { @@ -9970,9 +10000,9 @@ "dev": true }, "simple-get": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", - "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.2.tgz", + "integrity": "sha512-Ijd/rV5o+mSBBs4F/x9oDPtTx9Zb6X9brmnXvMW4J7IR15ngi9q5xxqWBKU744jTZiaXtxaPL7uHG6vtN8kUkw==", "dev": true, "requires": { "decompress-response": "^3.3.0", @@ -10533,6 +10563,62 @@ } } }, + "tar": { + "version": "4.4.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", + "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.4", + "fs-minipass": "^1.2.7", + "minipass": "^2.9.0", + "minizlib": "^1.3.3", + "mkdirp": "^0.5.5", + "safe-buffer": "^5.2.1", + "yallist": "^3.1.1" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "optional": true + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, + "optional": true + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "optional": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "optional": true + } + } + }, "tar-fs": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", @@ -10640,9 +10726,9 @@ } }, "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, "to-buffer": { @@ -10725,9 +10811,9 @@ "dev": true }, "tree-kill": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", - "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true }, "trim": { @@ -11265,9 +11351,9 @@ } }, "ws": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", - "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", + "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", "dev": true, "requires": { "async-limiter": "~1.0.0" @@ -11292,9 +11378,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yallist": { diff --git a/package-scripts.js b/package-scripts.js index 0e6b597..0dd4285 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -57,7 +57,15 @@ module.exports = { description: 'Generates table of contents in README', script: 'doctoc README.md' }, - copyTypes: npsUtils.copy('src/*.js.flow src/*.d.ts dist'), + copyTypes: series( + npsUtils.copy('src/*.js.flow src/*.d.ts dist'), + npsUtils.copy( + 'dist/index.js.flow dist --rename="final-form-arrays.cjs.js.flow"' + ), + npsUtils.copy( + 'dist/index.js.flow dist --rename="final-form-arrays.es.js.flow"' + ) + ), lint: { description: 'lint the entire project', script: 'eslint .' diff --git a/package.json b/package.json index 740246b..3fe5ff7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "final-form-arrays", - "version": "3.0.2", + "version": "3.1.0", "description": "Array Mutators for 🏁 Final Form", "main": "dist/final-form-arrays.cjs.js", "jsnext:main": "dist/final-form-arrays.es.js", @@ -52,7 +52,7 @@ "eslint-plugin-import": "^2.16.0", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-react": "^7.13.0", - "final-form": "^4.18.2", + "final-form": "^4.20.8", "flow-bin": "^0.102.0", "glow": "^1.2.2", "husky": "^3.0.0", @@ -73,7 +73,7 @@ "typescript": "^3.5.3" }, "peerDependencies": { - "final-form": "^4.18.2" + "final-form": "^4.20.8" }, "lint-staged": { "*.{js*,ts*,json,md,css}": [ diff --git a/src/copyField.js b/src/copyField.js new file mode 100644 index 0000000..33d3bb1 --- /dev/null +++ b/src/copyField.js @@ -0,0 +1,35 @@ +// @flow +import type { InternalFieldState } from 'final-form/dist/types' + +function copyField( + oldFields: { [string]: InternalFieldState }, + oldKey: string, + newFields: { [string]: InternalFieldState }, + newKey: string +) { + newFields[newKey] = { + ...oldFields[oldKey], + name: newKey, + // prevent functions from being overwritten + // if the newFields[newKey] does not exist, it will be created + // when that field gets registered, with its own change/blur/focus callbacks + change: oldFields[newKey] && oldFields[newKey].change, + blur: oldFields[newKey] && oldFields[newKey].blur, + focus: oldFields[newKey] && oldFields[newKey].focus, + lastFieldState: undefined // clearing lastFieldState forces renotification + } + + if (!newFields[newKey].change) { + delete newFields[newKey].change + } + + if (!newFields[newKey].blur) { + delete newFields[newKey].blur + } + + if (!newFields[newKey].focus) { + delete newFields[newKey].focus + } +} + +export default copyField diff --git a/src/insert.js b/src/insert.js index 7636cee..89a0443 100644 --- a/src/insert.js +++ b/src/insert.js @@ -1,12 +1,12 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import moveFieldState from './moveFieldState' +import copyField from './copyField' import { escapeRegexTokens } from './utils' const insert: Mutator = ( [name, index, value]: any[], state: MutableState, - { changeValue, resetFieldState }: Tools + { changeValue }: Tools ) => { changeValue(state, name, (array: ?(any[])): any[] => { const copy = [...(array || [])] @@ -14,27 +14,27 @@ const insert: Mutator = ( return copy }) - const backup = { ...state.fields } - // now we have increment any higher indexes const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) - - // we need to increment high indices first so - // lower indices won't overlap - Object.keys(state.fields) - .sort() - .reverse() - .forEach(key => { - const tokens = pattern.exec(key) - if (tokens) { - const fieldIndex = Number(tokens[1]) - if (fieldIndex >= index) { - // inc index one higher - const incrementedKey = `${name}[${fieldIndex + 1}]${tokens[2]}` - moveFieldState(state, backup[key], incrementedKey) - } + const newFields = {} + Object.keys(state.fields).forEach(key => { + const tokens = pattern.exec(key) + if (tokens) { + const fieldIndex = Number(tokens[1]) + if (fieldIndex >= index) { + // Shift all higher indices up + const incrementedKey = `${name}[${fieldIndex + 1}]${tokens[2]}` + copyField(state.fields, key, newFields, incrementedKey) + return } - }) + } + + // Keep this field that does not match the name, + // or has index smaller than what is being inserted + newFields[key] = state.fields[key] + }) + + state.fields = newFields } export default insert diff --git a/src/insert.test.js b/src/insert.test.js index d35d094..41663fd 100644 --- a/src/insert.test.js +++ b/src/insert.test.js @@ -84,7 +84,7 @@ describe('insert', () => { }) it('should increment other field data from the specified index', () => { - const array = ['a', 'b', 'c', 'd'] + const array = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'] // implementation of changeValue taken directly from Final Form const changeValue = (state, name, mutate) => { const before = getIn(state.formState.values, name) @@ -111,15 +111,15 @@ describe('insert', () => { touched: true, error: 'B Error' }, - 'foo[2]': { - name: 'foo[2]', + 'foo[9]': { + name: 'foo[9]', touched: true, - error: 'C Error' + error: 'J Error' }, - 'foo[3]': { - name: 'foo[3]', + 'foo[10]': { + name: 'foo[10]', touched: false, - error: 'D Error' + error: 'K Error' } } } @@ -132,7 +132,20 @@ describe('insert', () => { expect(state).toEqual({ formState: { values: { - foo: ['a', 'NEWVALUE', 'b', 'c', 'd'] + foo: [ + 'a', + 'NEWVALUE', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k' + ] } }, fields: { @@ -147,16 +160,16 @@ describe('insert', () => { error: 'B Error', lastFieldState: undefined }, - 'foo[3]': { - name: 'foo[3]', + 'foo[10]': { + name: 'foo[10]', touched: true, - error: 'C Error', + error: 'J Error', lastFieldState: undefined }, - 'foo[4]': { - name: 'foo[4]', + 'foo[11]': { + name: 'foo[11]', touched: false, - error: 'D Error', + error: 'K Error', lastFieldState: undefined } } diff --git a/src/move.js b/src/move.js index 4aaca6c..1529569 100644 --- a/src/move.js +++ b/src/move.js @@ -1,9 +1,7 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import moveFields from './moveFields' -import restoreFunctions from './restoreFunctions' - -const TMP: string = 'tmp' +import copyField from './copyField' +import { escapeRegexTokens } from './utils' const move: Mutator = ( [name, from, to]: any[], @@ -21,34 +19,44 @@ const move: Mutator = ( return copy }) - //make a copy of a state for further functions restore - const backupState = { ...state, fields: { ...state.fields } } - - // move this row to tmp index - const fromPrefix = `${name}[${from}]` - moveFields(name, fromPrefix, TMP, state) - - if (from < to) { - // moving to a higher index - // decrement all indices between from and to - for (let i = from + 1; i <= to; i++) { - const innerFromPrefix = `${name}[${i}]` - moveFields(name, innerFromPrefix, `${i - 1}`, state) - } + const newFields = {} + const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) + let lowest + let highest + let increment + if (from > to) { + lowest = to + highest = from + increment = 1 } else { - // moving to a lower index - // increment all indices between to and from - for (let i = from - 1; i >= to; i--) { - const innerFromPrefix = `${name}[${i}]` - moveFields(name, innerFromPrefix, `${i + 1}`, state) - } + lowest = from + highest = to + increment = -1 } + Object.keys(state.fields).forEach(key => { + const tokens = pattern.exec(key) + if (tokens) { + const fieldIndex = Number(tokens[1]) + if (fieldIndex === from) { + const newKey = `${name}[${to}]${tokens[2]}` + copyField(state.fields, key, newFields, newKey) + return + } + + if (lowest <= fieldIndex && fieldIndex <= highest) { + // Shift all indices + const newKey = `${name}[${fieldIndex + increment}]${tokens[2]}` + copyField(state.fields, key, newFields, newKey) + return + } + } - // move from tmp index to destination - const tmpPrefix = `${name}[${TMP}]` - moveFields(name, tmpPrefix, to, state) + // Keep this field that does not match the name, + // or has index smaller or larger than affected range + newFields[key] = state.fields[key] + }) - restoreFunctions(state, backupState) + state.fields = newFields } export default move diff --git a/src/moveFieldState.js b/src/moveFieldState.js deleted file mode 100644 index c1219ea..0000000 --- a/src/moveFieldState.js +++ /dev/null @@ -1,33 +0,0 @@ -// @flow -import type { MutableState } from 'final-form' - -function moveFieldState( - state: MutableState, - source: Object, - destKey: string, - oldState: MutableState = state -) { - delete state.fields[source.name] - state.fields[destKey] = { - ...source, - name: destKey, - // prevent functions from being overwritten - // if the state.fields[destKey] does not exist, it will be created - // when that field gets registered, with its own change/blur/focus callbacks - change: oldState.fields[destKey] && oldState.fields[destKey].change, - blur: oldState.fields[destKey] && oldState.fields[destKey].blur, - focus: oldState.fields[destKey] && oldState.fields[destKey].focus, - lastFieldState: undefined // clearing lastFieldState forces renotification - } - if (!state.fields[destKey].change) { - delete state.fields[destKey].change; - } - if (!state.fields[destKey].blur) { - delete state.fields[destKey].blur; - } - if (!state.fields[destKey].focus) { - delete state.fields[destKey].focus; - } -} - -export default moveFieldState diff --git a/src/moveFields.js b/src/moveFields.js deleted file mode 100644 index c1a6203..0000000 --- a/src/moveFields.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import type { MutableState } from 'final-form' -import moveFieldState from './moveFieldState'; - -function moveFields( - name: string, - matchPrefix: string, - destIndex: string, - state: MutableState -) { - Object.keys(state.fields).forEach(key => { - if (key.substring(0, matchPrefix.length) === matchPrefix) { - const suffix = key.substring(matchPrefix.length) - const destKey = `${name}[${destIndex}]${suffix}` - moveFieldState(state, state.fields[key], destKey) - } - }) -} - -export default moveFields diff --git a/src/pop.js b/src/pop.js index 57fe265..cfdb486 100644 --- a/src/pop.js +++ b/src/pop.js @@ -1,37 +1,17 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import { escapeRegexTokens } from './utils' +import remove from './remove' const pop: Mutator = ( [name]: any[], state: MutableState, - { changeValue }: Tools + tools: Tools ) => { - let result - let removedIndex: ?number - changeValue(state, name, (array: ?(any[])): ?(any[]) => { - if (array) { - if (!array.length) { - return [] - } - removedIndex = array.length - 1 - result = array[removedIndex] - return array.slice(0, removedIndex) - } - }) - - // now we have to remove any subfields for our index, - if (removedIndex !== undefined) { - const pattern = new RegExp( - `^${escapeRegexTokens(name)}\\[${removedIndex}].*` - ) - Object.keys(state.fields).forEach(key => { - if (pattern.test(key)) { - delete state.fields[key] - } - }) - } - return result + const { getIn } = tools; + const array = getIn(state.formState.values, name) + return array && array.length > 0 + ? remove([name, array.length - 1], state, tools) + : undefined } export default pop diff --git a/src/pop.test.js b/src/pop.test.js index f1a8b77..adfd1da 100644 --- a/src/pop.test.js +++ b/src/pop.test.js @@ -23,7 +23,7 @@ describe('pop', () => { } } } - const result = pop(['foo'], state, { changeValue }) + const result = pop(['foo'], state, { changeValue, getIn, setIn }) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -33,71 +33,59 @@ describe('pop', () => { }) it('should return undefined if array is undefined', () => { - const changeValue = jest.fn() + // implementation of changeValue taken directly from Final Form + const changeValue = (state, name, mutate) => { + const before = getIn(state.formState.values, name) + const after = mutate(before) + state.formState.values = setIn(state.formState.values, name, after) || {} + } const state = { formState: { values: { - foo: ['one', 'two'] + foo: undefined } }, - fields: { - 'foo[0]': { - name: 'foo[0]', - touched: true, - error: 'First Error' - }, - 'foo[1]': { - name: 'foo[1]', - touched: false, - error: 'Second Error' - } - } + fields: {} } - const returnValue = pop(['foo'], state, { changeValue }) - const op = changeValue.mock.calls[0][2] + const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) expect(returnValue).toBeUndefined() - const result = op(undefined) + const result = state.formState.foo expect(result).toBeUndefined() }) it('should return empty array if array is empty', () => { - const changeValue = jest.fn() + // implementation of changeValue taken directly from Final Form + const changeValue = (state, name, mutate) => { + const before = getIn(state.formState.values, name) + const after = mutate(before) + state.formState.values = setIn(state.formState.values, name, after) || {} + } const state = { formState: { values: { - foo: ['one', 'two'] + foo: [] } }, - fields: { - 'foo[0]': { - name: 'foo[0]', - touched: true, - error: 'First Error' - }, - 'foo[1]': { - name: 'foo[1]', - touched: false, - error: 'Second Error' - } - } + fields: {} } - const returnValue = pop(['foo'], state, { changeValue }) - const op = changeValue.mock.calls[0][2] + const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) expect(returnValue).toBeUndefined() - const result = op([]) + const result = state.formState.values.foo expect(Array.isArray(result)).toBe(true) expect(result.length).toBe(0) }) it('should pop value off the end of array and return it', () => { - let result - const changeValue = jest.fn((args, state, op) => { - result = op(['a', 'b', 'c']) - }) + // implementation of changeValue taken directly from Final Form + const changeValue = jest.fn((state, name, mutate) => { + const before = getIn(state.formState.values, name) + const after = mutate(before) + state.formState.values = setIn(state.formState.values, name, after) || {} + }) const state = { formState: { values: { - foo: ['one', 'two'] + foo: ['a', 'b', 'c'] } }, fields: { @@ -113,7 +101,8 @@ describe('pop', () => { } } } - const returnValue = pop(['foo'], state, { changeValue }) + const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) + const result = state.formState.values.foo expect(returnValue).toBe('c') expect(Array.isArray(result)).toBe(true) expect(result).toEqual(['a', 'b']) @@ -161,7 +150,7 @@ describe('pop', () => { } } } - const returnValue = pop(['foo'], state, { changeValue }) + const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) expect(returnValue).toBe('d') expect(Array.isArray(state.formState.values.foo)).toBe(true) expect(state.formState.values.foo).not.toBe(array) // copied @@ -238,7 +227,7 @@ describe('pop', () => { } } } - const returnValue = pop(['foo[0]'], state, { changeValue }) + const returnValue = pop(['foo[0]'], state, { changeValue, getIn, setIn }) expect(returnValue).toBe('d') expect(Array.isArray(state.formState.values.foo)).toBe(true) expect(state.formState.values.foo).not.toBe(array) // copied diff --git a/src/remove.js b/src/remove.js index 8af1f60..dce9a24 100644 --- a/src/remove.js +++ b/src/remove.js @@ -1,45 +1,65 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import moveFieldState from './moveFieldState' +import copyField from './copyField' import { escapeRegexTokens } from './utils' const remove: Mutator = ( [name, index]: any[], state: MutableState, - { changeValue, renameField }: Tools + { changeValue, getIn, setIn }: Tools ) => { let returnValue - changeValue(state, name, (array: ?(any[])): any[] => { - const copy = [...(array || [])] + changeValue(state, name, (array: ?(any[])): ?(any[]) => { + if (!array) { + return array + } + + const copy = [...array] returnValue = copy[index] copy.splice(index, 1) - return copy + return copy.length > 0 + ? copy + : undefined }) // now we have to remove any subfields for our index, // and decrement all higher indexes. const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) - const backup = { ...state, fields: { ...state.fields } } + const newFields = {} Object.keys(state.fields).forEach(key => { const tokens = pattern.exec(key) if (tokens) { const fieldIndex = Number(tokens[1]) if (fieldIndex === index) { - // delete any subfields for this array item - delete state.fields[key] - } else if (fieldIndex > index) { - // shift all higher ones down - delete state.fields[key] - const decrementedKey = `${name}[${fieldIndex - 1}]${tokens[2]}` - if (backup.fields[decrementedKey]) { - moveFieldState(state, backup.fields[key], decrementedKey, backup) - } else { - // take care of setting the correct change, blur, focus, validators on new field - renameField(state, key, decrementedKey) + // delete any submitErrors for this array item + // if the root key of the delete index + if (key === `${name}[${index}]`) { + const path = `formState.submitErrors.${name}` + const submitErrors = getIn(state, path) + // if has submitErrors for array + if (Array.isArray(submitErrors)) { + submitErrors.splice(index, 1) + state = setIn(state, path, submitErrors) + } } + + return + } + + if (fieldIndex > index) { + // Shift all higher indices down + const decrementedKey = `${name}[${fieldIndex - 1}]${tokens[2]}` + copyField(state.fields, key, newFields, decrementedKey) + return } } + + // Keep this field that does not match the name, + // or has index smaller than what is being removed + newFields[key] = state.fields[key] }) + + state.fields = newFields return returnValue } diff --git a/src/remove.test.js b/src/remove.test.js index fd4a685..245f4ff 100644 --- a/src/remove.test.js +++ b/src/remove.test.js @@ -23,7 +23,7 @@ describe('remove', () => { } } } - const result = remove(['foo', 0], state, { changeValue }) + const result = remove(['foo', 0], state, { changeValue, getIn, setIn }) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -42,12 +42,11 @@ describe('remove', () => { }, fields: {} } - const returnValue = remove(['foo', 1], state, { changeValue }) + const returnValue = remove(['foo', 1], state, { changeValue, getIn, setIn }) expect(returnValue).toBeUndefined() const op = changeValue.mock.calls[0][2] const result = op(undefined) - expect(Array.isArray(result)).toBe(true) - expect(result.length).toBe(0) + expect(result).toBeUndefined() }) it('should remove value from the specified index, and return it', () => { @@ -94,14 +93,6 @@ describe('remove', () => { touched: false, error: 'B Error' }, - 'foo[2]': { - name: 'foo[2]', - blur: blur2, - change: change2, - focus: focus2, - touched: true, - error: 'C Error' - }, 'foo[3]': { name: 'foo[3]', blur: blur3, @@ -110,13 +101,21 @@ describe('remove', () => { touched: false, error: 'D Error' }, + 'foo[2]': { + name: 'foo[2]', + blur: blur2, + change: change2, + focus: focus2, + touched: true, + error: 'C Error' + }, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = remove(['foo', 1], state, { changeValue }) + const returnValue = remove(['foo', 1], state, { changeValue, getIn, setIn }) expect(returnValue).toBe('b') expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ @@ -135,15 +134,6 @@ describe('remove', () => { touched: true, error: 'A Error' }, - 'foo[1]': { - name: 'foo[1]', - blur: blur1, - change: change1, - focus: focus1, - touched: true, - error: 'C Error', - lastFieldState: undefined - }, 'foo[2]': { name: 'foo[2]', blur: blur2, @@ -153,6 +143,15 @@ describe('remove', () => { error: 'D Error', lastFieldState: undefined }, + 'foo[1]': { + name: 'foo[1]', + blur: blur1, + change: change1, + focus: focus1, + touched: true, + error: 'C Error', + lastFieldState: undefined + }, anotherField: { name: 'anotherField', touched: false @@ -160,7 +159,6 @@ describe('remove', () => { } }) }) - it('should remove value from the specified index, and return it (nested arrays)', () => { const array = ['a', 'b', 'c', 'd'] @@ -190,6 +188,14 @@ describe('remove', () => { } }, fields: { + 'foo[0][3]': { + name: 'foo[0][3]', + blur: blur3, + change: change3, + focus: focus3, + touched: false, + error: 'D Error' + }, 'foo[0][0]': { name: 'foo[0][0]', blur: blur0, @@ -214,21 +220,17 @@ describe('remove', () => { touched: true, error: 'C Error' }, - 'foo[0][3]': { - name: 'foo[0][3]', - blur: blur3, - change: change3, - focus: focus3, - touched: false, - error: 'D Error' - }, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = remove(['foo[0]', 1], state, { changeValue }) + const returnValue = remove(['foo[0]', 1], state, { + changeValue, + getIn, + setIn + }) expect(returnValue).toBe('b') expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ @@ -239,6 +241,15 @@ describe('remove', () => { } }, fields: { + 'foo[0][2]': { + name: 'foo[0][2]', + blur: blur2, + change: change2, + focus: focus2, + touched: false, + error: 'D Error', + lastFieldState: undefined + }, 'foo[0][0]': { name: 'foo[0][0]', blur: blur0, @@ -256,24 +267,92 @@ describe('remove', () => { error: 'C Error', lastFieldState: undefined }, - 'foo[0][2]': { - name: 'foo[0][2]', + anotherField: { + name: 'anotherField', + touched: false + } + } + }) + }) + + it('should remove value from the specified index with submitError if one error in array', () => { + const array = ['a', { key: 'val' }] + const changeValue = jest.fn() + const renameField = jest.fn() + function blur0() {} + function change0() {} + function focus0() {} + function blur1() {} + function change1() {} + function focus1() {} + function blur2() {} + function change2() {} + function focus2() {} + const state = { + formState: { + values: { + foo: array, + anotherField: 42 + }, + submitErrors: { + foo: [ + { + key: 'A Submit Error' + } + ] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + blur: blur0, + change: change0, + focus: focus0, + touched: true, + error: 'A Error' + }, + 'foo[0].key': { + name: 'foo[0].key', blur: blur2, change: change2, focus: focus2, touched: false, - error: 'D Error', - lastFieldState: undefined + error: 'A Error' + }, + 'foo[1]': { + name: 'foo[1]', + blur: blur1, + change: change1, + focus: focus1, + touched: false, + error: 'B Error' + }, + 'foo[1].key': { + name: 'foo[1].key', + blur: blur2, + change: change2, + focus: focus2, + touched: false, + error: 'B Error' }, anotherField: { name: 'anotherField', touched: false } } + } + + const returnValue = remove(['foo', 0], state, { + renameField, + changeValue, + getIn, + setIn }) - }) + expect(returnValue).toBeUndefined() + expect(getIn(state, 'formState.submitErrors')).toEqual({ foo: [] }) + }) - it('should remove value from the specified index, and handle new fields', () => { + it('should remove value from the specified index with submitError if two errors in array', () => { const array = ['a', { key: 'val' }] const changeValue = jest.fn() const renameField = jest.fn() @@ -291,6 +370,16 @@ describe('remove', () => { values: { foo: array, anotherField: 42 + }, + submitErrors: { + foo: [ + { + key: 'A Submit Error' + }, + { + key: 'B Submit Error' + } + ] } }, fields: { @@ -302,6 +391,14 @@ describe('remove', () => { touched: true, error: 'A Error' }, + 'foo[0].key': { + name: 'foo[0].key', + blur: blur2, + change: change2, + focus: focus2, + touched: false, + error: 'A Error' + }, 'foo[1]': { name: 'foo[1]', blur: blur1, @@ -324,11 +421,16 @@ describe('remove', () => { } } } - const returnValue = remove(['foo', 0], state, { renameField, changeValue }) + + const returnValue = remove(['foo', 0], state, { + renameField, + changeValue, + getIn, + setIn + }) expect(returnValue).toBeUndefined() - expect(renameField).toHaveBeenCalledTimes(1) - expect(renameField.mock.calls[0][0]).toEqual(state) - expect(renameField.mock.calls[0][1]).toEqual('foo[1].key') - expect(renameField.mock.calls[0][2]).toEqual('foo[0].key') + expect(getIn(state, 'formState.submitErrors')).toEqual({ + foo: [{ key: 'B Submit Error' }] + }) }) }) diff --git a/src/removeBatch.js b/src/removeBatch.js index 8c2480e..48057f1 100644 --- a/src/removeBatch.js +++ b/src/removeBatch.js @@ -1,22 +1,46 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import moveFieldState from './moveFieldState' +import copyField from './copyField' import { escapeRegexTokens } from './utils' -const countBelow = (array, value) => - array.reduce((count, item) => (item < value ? count + 1 : count), 0) +const binarySearch = (list: number[], value: number): number => { + // This algorithm assumes the items in list is unique + let first = 0 + let last = list.length - 1 + let middle = 0 + + while (first <= last) { + middle = Math.floor((first + last) / 2) + if (list[middle] === value) { + return middle + } + + if (list[middle] > value) { + last = middle - 1 + } else { + first = middle + 1 + } + } + + return ~first +} const removeBatch: Mutator = ( [name, indexes]: any[], state: MutableState, { changeValue }: Tools ) => { + if (indexes.length === 0) { + return [] + } + const sortedIndexes: number[] = [...indexes] sortedIndexes.sort() - // remove duplicates - for (let i = 0; i < sortedIndexes.length; i++) { - if (i > 0 && sortedIndexes[i] === sortedIndexes[i - 1]) { - sortedIndexes.splice(i--, 1) + + // Remove duplicates + for (let i = sortedIndexes.length - 1; i > 0; i -= 1) { + if (sortedIndexes[i] === sortedIndexes[i - 1]) { + sortedIndexes.splice(i, 1) } } @@ -24,39 +48,52 @@ const removeBatch: Mutator = ( changeValue(state, name, (array: ?(any[])): ?(any[]) => { // use original order of indexes for return value returnValue = indexes.map(index => array && array[index]) - if (!array || !sortedIndexes.length) { + + if (!array) { return array } const copy = [...array] - const removed = [] - sortedIndexes.forEach((index: number) => { - copy.splice(index - removed.length, 1) - removed.push(array && array[index]) - }) - return copy + for (let i = sortedIndexes.length - 1; i >= 0; i -= 1) { + const index = sortedIndexes[i] + copy.splice(index, 1) + } + + return copy.length > 0 + ? copy + : undefined }) // now we have to remove any subfields for our indexes, // and decrement all higher indexes. const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) - const newState = { ...state, fields: {} } + const newFields = {} Object.keys(state.fields).forEach(key => { const tokens = pattern.exec(key) if (tokens) { const fieldIndex = Number(tokens[1]) - if (!~sortedIndexes.indexOf(fieldIndex)) { - // not one of the removed indexes - // shift all higher ones down - const decrementedKey = `${name}[${fieldIndex - - countBelow(sortedIndexes, fieldIndex)}]${tokens[2]}` - moveFieldState(newState, state.fields[key], decrementedKey, state) + const indexOfFieldIndex = binarySearch(sortedIndexes, fieldIndex) + if (indexOfFieldIndex >= 0) { + // One of the removed indices + return + } + + if (fieldIndex > sortedIndexes[0]) { + // Shift all higher indices down + const decrementedKey = `${name}[${fieldIndex - ~indexOfFieldIndex}]${ + tokens[2] + }` + copyField(state.fields, key, newFields, decrementedKey) + return } - } else { - newState.fields[key] = state.fields[key] } + + // Keep this field that does not match the name, + // or has index smaller than what is being removed + newFields[key] = state.fields[key] }) - state.fields = newState.fields + + state.fields = newFields return returnValue } diff --git a/src/removeBatch.test.js b/src/removeBatch.test.js index 15f0c8d..aaec2e5 100644 --- a/src/removeBatch.test.js +++ b/src/removeBatch.test.js @@ -187,11 +187,51 @@ describe('removeBatch', () => { expect(result).toBeUndefined() }) - it('should return the original array if no indexes are specified to be removed', () => { - const op = getOp([]) - const result = op(['a', 'b', 'c', 'd', 'e']) - expect(Array.isArray(result)).toBe(true) - expect(result).toEqual(['a', 'b', 'c', 'd', 'e']) + it('should keep the original state if no indexes are specified to be removed', () => { + const array = ['a', 'b', 'c', 'd', 'e'] + function blur0() {} + function change0() {} + function focus0() {} + const state = { + formState: { + values: { + foo: array + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + blur: blur0, + change: change0, + focus: focus0, + touched: true, + error: 'A Error' + } + } + } + const changeValue = jest.fn() + const returnValue = removeBatch(['foo[0]', []], state, { + changeValue + }) + expect(returnValue).toEqual([]) + expect(state.formState.values.foo).toBe(array) // no change + expect(state).toEqual({ + formState: { + values: { + foo: array + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + blur: blur0, + change: change0, + focus: focus0, + touched: true, + error: 'A Error' + } + } + }) }) it('should remove the values at the specified indexes', () => { @@ -239,6 +279,14 @@ describe('removeBatch', () => { } }, fields: { + 'foo[4]': { + name: 'foo[4]', + blur: blur4, + change: change4, + focus: focus4, + touched: true, + error: 'E Error' + }, 'foo[0]': { name: 'foo[0]', blur: blur0, @@ -271,31 +319,32 @@ describe('removeBatch', () => { touched: false, error: 'D Error' }, - 'foo[4]': { - name: 'foo[4]', - blur: blur4, - change: change4, - focus: focus4, - touched: true, - error: 'E Error' - }, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = removeBatch(['foo', [1, 2]], state, { changeValue }) - expect(returnValue).toEqual(['b', 'c']) + const returnValue = removeBatch(['foo', [1, 3]], state, { changeValue }) + expect(returnValue).toEqual(['b', 'd']) expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ formState: { values: { - foo: ['a', 'd', 'e'], + foo: ['a', 'c', 'e'], anotherField: 42 } }, fields: { + 'foo[2]': { + name: 'foo[2]', + blur: blur2, + change: change2, + focus: focus2, + touched: true, + error: 'E Error', + lastFieldState: undefined + }, 'foo[0]': { name: 'foo[0]', blur: blur0, @@ -310,17 +359,8 @@ describe('removeBatch', () => { blur: blur1, change: change1, focus: focus1, - touched: false, - error: 'D Error', - lastFieldState: undefined - }, - 'foo[2]': { - name: 'foo[2]', - blur: blur2, - change: change2, - focus: focus2, touched: true, - error: 'E Error', + error: 'C Error', lastFieldState: undefined }, anotherField: { @@ -362,6 +402,14 @@ describe('removeBatch', () => { } }, fields: { + 'foo[0][4]': { + name: 'foo[0][4]', + blur: blur4, + change: change4, + focus: focus4, + touched: true, + error: 'E Error' + }, 'foo[0][0]': { name: 'foo[0][0]', blur: blur0, @@ -394,33 +442,34 @@ describe('removeBatch', () => { touched: false, error: 'D Error' }, - 'foo[0][4]': { - name: 'foo[0][4]', - blur: blur4, - change: change4, - focus: focus4, - touched: true, - error: 'E Error' - }, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = removeBatch(['foo[0]', [1, 2]], state, { + const returnValue = removeBatch(['foo[0]', [1, 3]], state, { changeValue }) - expect(returnValue).toEqual(['b', 'c']) + expect(returnValue).toEqual(['b', 'd']) expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ formState: { values: { - foo: [['a', 'd', 'e']], + foo: [['a', 'c', 'e']], anotherField: 42 } }, fields: { + 'foo[0][2]': { + name: 'foo[0][2]', + blur: blur2, + change: change2, + focus: focus2, + touched: true, + error: 'E Error', + lastFieldState: undefined + }, 'foo[0][0]': { name: 'foo[0][0]', blur: blur0, @@ -435,17 +484,8 @@ describe('removeBatch', () => { blur: blur1, change: change1, focus: focus1, - touched: false, - error: 'D Error', - lastFieldState: undefined - }, - 'foo[0][2]': { - name: 'foo[0][2]', - blur: blur2, - change: change2, - focus: focus2, touched: true, - error: 'E Error', + error: 'C Error', lastFieldState: undefined }, anotherField: { diff --git a/src/restoreFunctions.js b/src/restoreFunctions.js deleted file mode 100644 index 5a21cc9..0000000 --- a/src/restoreFunctions.js +++ /dev/null @@ -1,26 +0,0 @@ -// @flow -import type { MutableState } from 'final-form' - -function restoreFunctions( - state: MutableState, - backupState: MutableState -) { - Object.keys(state.fields).forEach(key => { - state.fields[key] = { - ...state.fields[key], - change: state.fields[key].change || (backupState.fields[key] && backupState.fields[key].change), - blur: state.fields[key].blur || (backupState.fields[key] && backupState.fields[key].blur), - focus: state.fields[key].focus || (backupState.fields[key] && backupState.fields[key].focus) - } - if (!state.fields[key].change) { - delete state.fields[key].change; - } - if (!state.fields[key].blur) { - delete state.fields[key].blur; - } - if (!state.fields[key].focus) { - delete state.fields[key].focus; - } - }) -} -export default restoreFunctions diff --git a/src/shift.test.js b/src/shift.test.js index 8cdd551..84950cc 100644 --- a/src/shift.test.js +++ b/src/shift.test.js @@ -23,7 +23,7 @@ describe('shift', () => { } } } - const result = shift(['foo'], state, { changeValue }) + const result = shift(['foo'], state, { changeValue, getIn, setIn }) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -42,12 +42,11 @@ describe('shift', () => { }, fields: {} } - const returnValue = shift(['foo'], state, { changeValue }) + const returnValue = shift(['foo'], state, { changeValue, getIn, setIn }) expect(returnValue).toBeUndefined() const op = changeValue.mock.calls[0][2] const result = op(undefined) - expect(Array.isArray(result)).toBe(true) - expect(result.length).toBe(0) + expect(result).toBeUndefined() }) it('should remove first value from array and return it', () => { @@ -92,7 +91,7 @@ describe('shift', () => { } } } - const returnValue = shift(['foo'], state, { changeValue }) + const returnValue = shift(['foo'], state, { changeValue, getIn, setIn }) expect(returnValue).toBe('a') expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ diff --git a/src/swap.js b/src/swap.js index e51fe25..736adec 100644 --- a/src/swap.js +++ b/src/swap.js @@ -1,10 +1,6 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import moveFieldState from './moveFieldState' -import moveFields from './moveFields'; -import restoreFunctions from './restoreFunctions'; - -const TMP: string = 'tmp' +import copyField from './copyField' const swap: Mutator = ( [name, indexA, indexB]: any[], @@ -22,19 +18,26 @@ const swap: Mutator = ( return copy }) - //make a copy of a state for further functions restore - const backupState = { ...state, fields: { ...state.fields } } - // swap all field state that begin with "name[indexA]" with that under "name[indexB]" const aPrefix = `${name}[${indexA}]` const bPrefix = `${name}[${indexB}]` - const tmpPrefix = `${name}[${TMP}]` - - moveFields(name, aPrefix, TMP, state) - moveFields(name, bPrefix, indexA, state) - moveFields(name, tmpPrefix, indexB, state) + const newFields = {} + Object.keys(state.fields).forEach(key => { + if (key.substring(0, aPrefix.length) === aPrefix) { + const suffix = key.substring(aPrefix.length) + const newKey = bPrefix + suffix + copyField(state.fields, key, newFields, newKey) + } else if (key.substring(0, bPrefix.length) === bPrefix) { + const suffix = key.substring(bPrefix.length) + const newKey = aPrefix + suffix + copyField(state.fields, key, newFields, newKey) + } else { + // Keep this field that does not match the name + newFields[key] = state.fields[key] + } + }) - restoreFunctions(state, backupState) + state.fields = newFields } export default swap diff --git a/src/swap.test.js b/src/swap.test.js index 013940a..b74dd7b 100644 --- a/src/swap.test.js +++ b/src/swap.test.js @@ -93,10 +93,10 @@ describe('swap', () => { } }, fields: { - 'foo[0]': { - name: 'foo[0]', - touched: false, - error: 'Error C', + 'foo[2]': { + name: 'foo[2]', + touched: true, + error: 'Error A', lastFieldState: undefined }, 'foo[1]': { @@ -105,10 +105,10 @@ describe('swap', () => { error: 'Error B', lastFieldState: 'anything' // unchanged }, - 'foo[2]': { - name: 'foo[2]', - touched: true, - error: 'Error A', + 'foo[0]': { + name: 'foo[0]', + touched: false, + error: 'Error C', lastFieldState: undefined }, 'foo[3]': { @@ -215,22 +215,22 @@ describe('swap', () => { } }, fields: { - 'foo[0].dog': { - name: 'foo[0].dog', + 'foo[2].dog': { + name: 'foo[2].dog', touched: true, - error: 'Error C Dog', + error: 'Error A Dog', lastFieldState: undefined }, - 'foo[0].cat': { - name: 'foo[0].cat', + 'foo[2].cat': { + name: 'foo[2].cat', touched: false, - error: 'Error C Cat', + error: 'Error A Cat', lastFieldState: undefined }, - 'foo[0].axe': { - name: 'foo[0].axe', + 'foo[2].rock': { + name: 'foo[2].rock', touched: false, - error: 'Error C Axe', + error: 'Error A Rock', lastFieldState: undefined }, 'foo[1].dog': { @@ -245,22 +245,22 @@ describe('swap', () => { error: 'Error B Cat', lastFieldState: 'anything' // unchanged }, - 'foo[2].dog': { - name: 'foo[2].dog', + 'foo[0].dog': { + name: 'foo[0].dog', touched: true, - error: 'Error A Dog', + error: 'Error C Dog', lastFieldState: undefined }, - 'foo[2].cat': { - name: 'foo[2].cat', + 'foo[0].cat': { + name: 'foo[0].cat', touched: false, - error: 'Error A Cat', + error: 'Error C Cat', lastFieldState: undefined }, - 'foo[2].rock': { - name: 'foo[2].rock', + 'foo[0].axe': { + name: 'foo[0].axe', touched: false, - error: 'Error A Rock', + error: 'Error C Axe', lastFieldState: undefined }, 'foo[3].dog': { diff --git a/src/utils.js b/src/utils.js index 35c90cb..592f664 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,5 +2,4 @@ // From MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping export const escapeRegexTokens = (string: string): string => - string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string - + string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string