diff --git a/gulpfile.js b/gulpfile.js index 1718d56..d4ffb3e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -36,7 +36,8 @@ async function compileTs(modules = false) { src([ 'src/**/*.{js,jsx,ts,tsx}', '!src/**/__stories__/**/*.{js,jsx,ts,tsx}', - '!src/**/__test__/**/*.*])', + '!src/**/__test__/**/*.*', + '!src/**/__tests__/**/*.*', ]) .pipe(sourcemaps.init()) .pipe( diff --git a/package-lock.json b/package-lock.json index 4a5121a..cee041a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "@types/react-grid-layout": "^1.3.5", "copyfiles": "^2.4.1", "eslint": "^8.57.1", - "gulp": "^4.0.2", + "gulp": "5.0.1", "gulp-cli": "^2.3.0", "gulp-dart-sass": "^1.0.2", "gulp-sourcemaps": "^3.0.0", @@ -3690,15 +3690,6 @@ "node": ">=10.13.0" } }, - "node_modules/@gravity-ui/gulp-utils/node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/@gravity-ui/gulp-utils/node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -3708,34 +3699,6 @@ "readable-stream": "3" } }, - "node_modules/@gravity-ui/gulp-utils/node_modules/to-through": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", - "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@gravity-ui/gulp-utils/node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", - "dev": true, - "dependencies": { - "clone": "^2.1.2", - "clone-stats": "^1.0.0", - "remove-trailing-separator": "^1.1.0", - "replace-ext": "^2.0.0", - "teex": "^1.0.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@gravity-ui/i18n": { "version": "1.7.0", "dev": true, @@ -3939,6 +3902,29 @@ "node": ">=0.10.0" } }, + "node_modules/@gulpjs/messages": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", + "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -10128,7 +10114,9 @@ } }, "node_modules/anymatch": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "license": "ISC", "dependencies": { @@ -10144,17 +10132,6 @@ "dev": true, "license": "MIT" }, - "node_modules/append-buffer": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/archy": { "version": "1.0.0", "dev": true, @@ -10204,17 +10181,6 @@ "node": ">=0.10.0" } }, - "node_modules/arr-filter": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/arr-flatten": { "version": "1.1.0", "dev": true, @@ -10223,17 +10189,6 @@ "node": ">=0.10.0" } }, - "node_modules/arr-map": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/arr-union": { "version": "3.1.0", "dev": true, @@ -10300,45 +10255,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-initial": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-slice": "^1.0.0", - "is-number": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-initial/node_modules/is-number": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-last": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-last/node_modules/is-number": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-slice": { "version": "1.1.0", "dev": true, @@ -10544,24 +10460,20 @@ "license": "MIT" }, "node_modules/async-done": { - "version": "1.3.2", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", + "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", "dev": true, "license": "MIT", "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^2.0.0", - "stream-exhaust": "^1.0.1" + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, - "node_modules/async-each": { - "version": "1.0.3", - "dev": true, - "license": "MIT" - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -10578,14 +10490,16 @@ "license": "MIT" }, "node_modules/async-settle": { - "version": "1.0.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", + "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", "dev": true, "license": "MIT", "dependencies": { - "async-done": "^1.2.2" + "async-done": "^2.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/asynckit": { @@ -10951,22 +10865,18 @@ } }, "node_modules/bach": { - "version": "1.2.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", "dev": true, "license": "MIT", "dependencies": { - "arr-filter": "^1.1.1", - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "array-each": "^1.0.0", - "array-initial": "^1.0.0", - "array-last": "^1.1.1", - "async-done": "^1.2.2", - "async-settle": "^1.0.0", - "now-and-later": "^2.0.0" + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/bail": { @@ -11134,15 +11044,6 @@ "node": ">=8" } }, - "node_modules/bindings": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/bl": { "version": "4.1.0", "dev": true, @@ -11336,14 +11237,6 @@ "node": "*" } }, - "node_modules/buffer-equal": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "dev": true, @@ -11997,14 +11890,6 @@ "node": ">=0.8" } }, - "node_modules/clone-buffer": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -12020,43 +11905,6 @@ "node": ">=6" } }, - "node_modules/clone-stats": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cloneable-readable": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, - "node_modules/cloneable-readable/node_modules/readable-stream": { - "version": "2.3.7", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/cloneable-readable/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/clsx": { "version": "2.1.1", "license": "MIT", @@ -12086,19 +11934,6 @@ "dev": true, "license": "MIT" }, - "node_modules/collection-map": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-map": "^2.0.2", - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/collection-visit": { "version": "1.0.0", "dev": true, @@ -13090,14 +12925,6 @@ "node": ">=0.10.0" } }, - "node_modules/default-resolution": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/defaults": { "version": "1.0.4", "dev": true, @@ -15464,12 +15291,6 @@ "ramda": "0.29.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/filelist": { "version": "1.0.4", "dev": true, @@ -15659,37 +15480,6 @@ "node": ">=0.4.0" } }, - "node_modules/flush-write-stream": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "node_modules/flush-write-stream/node_modules/readable-stream": { - "version": "2.3.7", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/flush-write-stream/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -15963,15 +15753,17 @@ } }, "node_modules/fs-mkdirp-stream": { - "version": "1.0.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/fs-monkey": { @@ -16265,306 +16057,55 @@ } }, "node_modules/glob-stream": { - "version": "6.1.0", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.4.tgz", + "integrity": "sha512-7uzFVQ1ZUBr0nYexxubOvocZXQ4HcHv6WO3TOiYUPaCda7BASuKgOw/NB4Pc9gHM3tK0Jrekq7el8pKhb8kx5w==", "dev": true, "license": "MIT", "dependencies": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/glob-stream/node_modules/glob-parent": { - "version": "3.1.0", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" } }, - "node_modules/glob-stream/node_modules/is-glob": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-stream/node_modules/readable-stream": { - "version": "2.3.7", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/glob-stream/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", + "node_modules/glob-to-regexp": { + "version": "0.4.1", "dev": true, "license": "BSD-2-Clause" }, "node_modules/glob-watcher": { - "version": "5.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "^2.0.0", - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "is-negated-glob": "^1.0.0", - "just-debounce": "^1.0.0", - "normalize-path": "^3.0.0", - "object.defaults": "^1.1.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/glob-watcher/node_modules/anymatch": { - "version": "2.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "node_modules/glob-watcher/node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/binary-extensions": { - "version": "1.13.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/braces": { - "version": "2.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/chokidar": { - "version": "2.1.8", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "optionalDependencies": { - "fsevents": "^1.2.7" - } - }, - "node_modules/glob-watcher/node_modules/extend-shallow": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/fill-range": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/fsevents": { - "version": "1.2.13", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/glob-watcher/node_modules/glob-parent": { - "version": "3.1.0", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/glob-watcher/node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/is-binary-path": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/is-number": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/isobject": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-watcher/node_modules/readable-stream": { - "version": "2.3.7", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/glob-watcher/node_modules/readdirp": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/glob-watcher/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/glob-watcher/node_modules/to-regex-range": { - "version": "2.1.1", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", + "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "async-done": "^2.0.0", + "chokidar": "^3.5.3" }, "engines": { - "node": ">=0.10.0" + "node": ">= 10.13.0" } }, "node_modules/global": { @@ -16723,20 +16264,22 @@ "license": "MIT" }, "node_modules/gulp": { - "version": "4.0.2", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.1.tgz", + "integrity": "sha512-PErok3DZSA5WGMd6XXV3IRNO0mlB+wW3OzhFJLEec1jSERg2j1bxJ6e5Fh6N6fn3FH2T9AP4UYNb/pYlADB9sA==", "dev": true, "license": "MIT", "dependencies": { - "glob-watcher": "^5.0.3", - "gulp-cli": "^2.2.0", - "undertaker": "^1.2.1", - "vinyl-fs": "^3.0.0" + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.1.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.0.2" }, "bin": { "gulp": "bin/gulp.js" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/gulp-cli": { @@ -17061,6 +16604,249 @@ "node": ">=0.10.0" } }, + "node_modules/gulp/node_modules/copy-props": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/each-props": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/fined": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0", + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/flagged-respawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/glogg": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", + "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", + "dev": true, + "license": "MIT", + "dependencies": { + "sparkles": "^2.1.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/gulp-cli": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.1.0.tgz", + "integrity": "sha512-zZzwlmEsTfXcxRKiCHsdyjZZnFvXWM4v1NqBJSYbuApkvVKivjcmOS2qruAJ+PkEHLFavcDKH40DPc1+t12a9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gulpjs/messages": "^1.1.0", + "chalk": "^4.1.2", + "copy-props": "^4.0.0", + "gulplog": "^2.2.0", + "interpret": "^3.1.1", + "liftoff": "^5.0.1", + "mute-stdout": "^2.0.0", + "replace-homedir": "^2.0.0", + "semver-greatest-satisfied-range": "^2.0.0", + "string-width": "^4.2.3", + "v8flags": "^4.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp/node_modules/gulplog": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", + "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "glogg": "^2.2.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp/node_modules/is-plain-object": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.1.0.tgz", + "integrity": "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp/node_modules/liftoff": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.1.tgz", + "integrity": "sha512-wwLXMbuxSF8gMvubFcFRp56lkFV69twvbU5vDPbaw+Q+/rF8j0HKjGbIdlSi+LuJm9jf7k9PB+nTxnsLMPcv2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/gulp/node_modules/mute-stdout": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", + "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/replace-homedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", + "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/semver-greatest-satisfied-range": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", + "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sver": "^1.8.3" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/sparkles": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", + "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gulp/node_modules/v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/gulplog": { "version": "1.0.0", "dev": true, @@ -18147,6 +17933,8 @@ }, "node_modules/is-negated-glob": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", "dev": true, "license": "MIT", "engines": { @@ -18420,6 +18208,8 @@ }, "node_modules/is-valid-glob": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", "dev": true, "license": "MIT", "engines": { @@ -20053,11 +19843,6 @@ "node": ">=4.0" } }, - "node_modules/just-debounce": { - "version": "1.1.0", - "dev": true, - "license": "MIT" - }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -20111,66 +19896,31 @@ "dependencies": { "language-subtag-registry": "^0.3.20" }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/last-run": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/lazy-universal-dotenv": { - "version": "4.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "app-root-dir": "^1.0.2", - "dotenv": "^16.0.0", - "dotenv-expand": "^10.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/lazystream": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.7", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "engines": { + "node": ">=0.10" } }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", + "node_modules/last-run": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", + "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/lazy-universal-dotenv": { + "version": "4.0.0", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "safe-buffer": "~5.1.0" + "app-root-dir": "^1.0.2", + "dotenv": "^16.0.0", + "dotenv-expand": "^10.0.0" + }, + "engines": { + "node": ">=14.0.0" } }, "node_modules/lcid": { @@ -20185,14 +19935,13 @@ } }, "node_modules/lead": { - "version": "1.0.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", "dev": true, "license": "MIT", - "dependencies": { - "flush-write-stream": "^1.0.2" - }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/leven": { @@ -22602,12 +22351,6 @@ "node": ">= 0.10" } }, - "node_modules/nan": { - "version": "2.22.0", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -22844,14 +22587,16 @@ } }, "node_modules/now-and-later": { - "version": "2.0.1", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, "license": "MIT", "dependencies": { - "once": "^1.3.2" + "once": "^1.4.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/npm-run-all": { @@ -23248,18 +22993,6 @@ "node": ">=0.10.0" } }, - "node_modules/object.reduce": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.values": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", @@ -23371,36 +23104,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ordered-read-streams": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.1" - } - }, - "node_modules/ordered-read-streams/node_modules/readable-stream": { - "version": "2.3.7", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/ordered-read-streams/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/os-locale": { "version": "1.4.0", "dev": true, @@ -23619,11 +23322,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-dirname": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, "node_modules/path-exists": { "version": "4.0.0", "dev": true, @@ -25202,31 +24900,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/remove-bom-buffer": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remove-bom-stream": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "dev": true, @@ -25362,14 +25035,16 @@ } }, "node_modules/resolve-options": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", "dev": true, "license": "MIT", "dependencies": { - "value-or-function": "^3.0.0" + "value-or-function": "^4.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/resolve-pkg-maps": { @@ -26442,8 +26117,20 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.13.2" + } + }, "node_modules/stream-exhaust": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", "dev": true, "license": "MIT" }, @@ -27056,6 +26743,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sver": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", + "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "semver": "^6.3.0" + } + }, "node_modules/sver-compat": { "version": "1.5.0", "dev": true, @@ -27440,15 +27137,6 @@ "xtend": "~4.0.1" } }, - "node_modules/through2-filter": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } - }, "node_modules/through2/node_modules/readable-stream": { "version": "2.3.7", "dev": true, @@ -27558,18 +27246,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/to-absolute-glob": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/to-object-path": { "version": "0.3.0", "dev": true, @@ -27618,14 +27294,16 @@ } }, "node_modules/to-through": { - "version": "2.0.0", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", "dev": true, "license": "MIT", "dependencies": { - "through2": "^2.0.3" + "streamx": "^2.12.5" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/tocbot": { @@ -28047,37 +27725,40 @@ } }, "node_modules/undertaker": { - "version": "1.3.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", "dev": true, "license": "MIT", "dependencies": { - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "bach": "^1.0.0", - "collection-map": "^1.0.0", - "es6-weak-map": "^2.0.1", - "fast-levenshtein": "^1.0.0", - "last-run": "^1.1.0", - "object.defaults": "^1.0.0", - "object.reduce": "^1.0.0", - "undertaker-registry": "^1.0.0" + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/undertaker-registry": { - "version": "1.0.1", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/undertaker/node_modules/fast-levenshtein": { - "version": "1.1.4", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } }, "node_modules/undici-types": { "version": "5.26.5", @@ -28214,15 +27895,6 @@ "imurmurhash": "^0.1.4" } }, - "node_modules/unique-stream": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "json-stable-stringify-without-jsonify": "^1.0.1", - "through2-filter": "^3.0.0" - } - }, "node_modules/unique-string": { "version": "2.0.0", "dev": true, @@ -28427,15 +28099,6 @@ "node": ">=8" } }, - "node_modules/upath": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4", - "yarn": "*" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -28719,11 +28382,13 @@ } }, "node_modules/value-or-function": { - "version": "3.0.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/vary": { @@ -28791,98 +28456,136 @@ } }, "node_modules/vinyl": { - "version": "2.2.1", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", "dev": true, "license": "MIT", "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, - "node_modules/vinyl-fs": { - "version": "3.0.3", + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", "dev": true, "license": "MIT", "dependencies": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" + "bl": "^5.0.0", + "vinyl": "^3.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, - "node_modules/vinyl-fs/node_modules/readable-stream": { - "version": "2.3.7", + "node_modules/vinyl-contents/node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", "dev": true, "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, - "node_modules/vinyl-fs/node_modules/string_decoder": { - "version": "1.1.1", + "node_modules/vinyl-contents/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.0" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/vinyl-sourcemap": { - "version": "1.1.0", + "node_modules/vinyl-fs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.2.tgz", + "integrity": "sha512-XRFwBLLTl8lRAOYiBqxY279wY46tVxLaRhSwo3GzKEuLz1giffsOquWWboD/haGf5lx+JyTigCFfe7DWHoARIA==", "dev": true, "license": "MIT", "dependencies": { - "append-buffer": "^1.0.2", - "convert-source-map": "^1.5.0", - "graceful-fs": "^4.1.6", - "normalize-path": "^2.1.1", - "now-and-later": "^2.0.0", - "remove-bom-buffer": "^3.0.0", - "vinyl": "^2.0.0" + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.3", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.1", + "vinyl-sourcemap": "^2.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, - "node_modules/vinyl-sourcemap/node_modules/normalize-path": { - "version": "2.1.1", + "node_modules/vinyl-fs/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { - "remove-trailing-separator": "^1.0.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" } }, + "node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemap/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/vinyl-sourcemaps-apply": { "version": "0.2.1", "dev": true, @@ -28899,6 +28602,16 @@ "node": ">=0.10.0" } }, + "node_modules/vinyl/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index 648efe0..f2b3b85 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "@types/react-grid-layout": "^1.3.5", "copyfiles": "^2.4.1", "eslint": "^8.57.1", - "gulp": "^4.0.2", + "gulp": "5.0.1", "gulp-cli": "^2.3.0", "gulp-dart-sass": "^1.0.2", "gulp-sourcemaps": "^3.0.0", diff --git a/src/components/DashKit/DashKit.tsx b/src/components/DashKit/DashKit.tsx index 6d9a6fe..dfca6d1 100644 --- a/src/components/DashKit/DashKit.tsx +++ b/src/components/DashKit/DashKit.tsx @@ -1,97 +1,27 @@ import React from 'react'; -import noop from 'lodash/noop'; import pick from 'lodash/pick'; import {DEFAULT_GROUP, DEFAULT_NAMESPACE} from '../../constants'; -import {DashKitDnDContext} from '../../context'; -import type { - Config, - ConfigItem, - ConfigLayout, - GlobalParams, - ItemDropProps, - ItemsStateAndParams, -} from '../../shared'; +import type {Config, ConfigLayout, ItemsStateAndParams} from '../../shared'; import type { AddConfigItem, AddNewItemOptions, - ContextProps, - DashKitEventHandler, - DashKitEventMap, - DashKitEventName, DashKitGroup, GridReflowOptions, - ItemManipulationCallback, - MenuItem, - Plugin, ReactGridLayoutProps, SetConfigItem, - Settings, - SettingsProps, } from '../../typings'; -import {RegisterManager, UpdateManager, reflowLayout} from '../../utils'; +import {UpdateManager, reflowLayout} from '../../utils'; import {DashKitDnDWrapper} from '../DashKitDnDWrapper/DashKitDnDWrapper'; import DashKitView from '../DashKitView/DashKitView'; -import GridLayout from '../GridLayout/GridLayout'; -import type {OverlayControlItem, PreparedCopyItemOptions} from '../OverlayControls/OverlayControls'; - -interface DashKitGeneralProps { - config: Config; - editMode: boolean; - draggableHandleClassName?: string; - overlayControls?: Record | null; - overlayMenuItems?: MenuItem[] | null; -} - -interface DashKitDefaultProps { - defaultGlobalParams: GlobalParams; - globalParams: GlobalParams; - itemsStateAndParams: ItemsStateAndParams; - settings: SettingsProps; - context: ContextProps; - noOverlay: boolean; - focusable?: boolean; - groups?: DashKitGroup[]; - - onItemEdit: (item: ConfigItem) => void; - onChange: (data: { - config: Config; - itemsStateAndParams: ItemsStateAndParams; - groups?: DashKitGroup[]; - }) => void; - - onDrop: (dropProps: ItemDropProps) => void; +import type GridLayout from '../GridLayout/GridLayout'; - onItemMountChange?: (item: ConfigItem, state: {isAsync: boolean; isMounted: boolean}) => void; - onItemRender?: (item: ConfigItem) => void; +import {DashKitBase, _emitSymbol} from './DashKitBase'; +import {registerManager} from './registration'; - getPreparedCopyItemOptions?: (options: PreparedCopyItemOptions) => PreparedCopyItemOptions; - onCopyFulfill?: (error: null | Error, data?: PreparedCopyItemOptions) => void; - - onItemFocus?: (item: ConfigItem) => void; - onItemBlur?: (item: ConfigItem) => void; - - onDragStart?: ItemManipulationCallback; - onDrag?: ItemManipulationCallback; - onDragStop?: ItemManipulationCallback; - onResizeStart?: ItemManipulationCallback; - onResize?: ItemManipulationCallback; - onResizeStop?: ItemManipulationCallback; -} - -export interface DashKitProps extends DashKitGeneralProps, Partial {} - -type DashKitInnerProps = DashKitGeneralProps & DashKitDefaultProps; - -const registerManager = new RegisterManager(); - -/** - * @internal Not part of the public API. Do not use in production code. - * Module-scoped symbol — not accessible to external consumers holding a ref, - * preventing synthetic event injection via instance._emit. - */ -export const _emitSymbol = Symbol('DashKit._emit'); +export type {DashKitProps} from './DashKitBase'; +export {_emitSymbol} from './DashKitBase'; const getReflowProps = (props: ReactGridLayoutProps): GridReflowOptions => Object.assign( @@ -115,41 +45,7 @@ const getReflowGroupsConfig = (groups: DashKitGroup[] = []) => { }; }; -export class DashKit extends React.PureComponent { - static defaultProps: DashKitDefaultProps = { - onItemEdit: noop, - onChange: noop, - onDrop: noop, - defaultGlobalParams: {}, - globalParams: {}, - itemsStateAndParams: {}, - settings: { - autoupdateInterval: 0, - silentLoading: false, - }, - context: {}, - noOverlay: false, - focusable: false, - }; - - static contextType = DashKitDnDContext; - - static registerPlugins(...plugins: Plugin[]) { - plugins.forEach((plugin) => { - registerManager.registerPlugin(plugin); - }); - } - - static reloadPlugins(...plugins: Plugin[]) { - plugins.forEach((plugin) => { - registerManager.reloadPlugin(plugin); - }); - } - - static setSettings(settings: Settings) { - registerManager.setSettings(settings); - } - +export class DashKit extends DashKitBase { static setItem({ item: setItem, namespace = DEFAULT_NAMESPACE, @@ -223,10 +119,6 @@ export class DashKit extends React.PureComponent { }); } - metaRef = React.createRef(); - - private _handlers = new Map void>>(); - render() { const content = ( { return content; } - - getItemsMeta() { - return this.metaRef.current?.getItemsMeta() ?? []; - } - - reloadItems(options?: {targetIds?: string[]; force?: boolean}) { - this.metaRef.current?.reloadItems(options); - } - - /** - * @experimental This API can change in minor releases. - * @param eventName Event name. - * @param handler Event handler. - * @returns Unsubscribe callback. - */ - on(eventName: T, handler: DashKitEventHandler): () => void { - if (!this._handlers.has(eventName)) { - this._handlers.set(eventName, new Set()); - } - this._handlers.get(eventName)?.add(handler); - return () => this._off(eventName, handler); - } - - [_emitSymbol] = (eventName: T, event: DashKitEventMap[T]): void => { - const handlers = this._handlers.get(eventName); - if (!handlers) { - return; - } - for (const handler of handlers) { - try { - (handler as DashKitEventHandler)(event); - } catch (e) { - // eslint-disable-next-line no-console - console.error('DashKit: event handler error', e); - } - } - }; - - private _off(eventName: T, handler: DashKitEventHandler): void { - const handlers = this._handlers.get(eventName); - if (handlers) { - handlers.delete(handler); - if (handlers.size === 0) { - this._handlers.delete(eventName); - } - } - } } diff --git a/src/components/DashKit/DashKitBase.tsx b/src/components/DashKit/DashKitBase.tsx new file mode 100644 index 0000000..292b146 --- /dev/null +++ b/src/components/DashKit/DashKitBase.tsx @@ -0,0 +1,176 @@ +import React from 'react'; + +import noop from 'lodash/noop'; + +import {DashKitDnDContext} from '../../context'; +import type { + Config, + ConfigItem, + GlobalParams, + ItemDropProps, + ItemsStateAndParams, +} from '../../shared'; +import type { + ContextProps, + DashKitEventHandler, + DashKitEventMap, + DashKitEventName, + DashKitGroup, + ItemManipulationCallback, + MenuItem, + Plugin, + Settings, + SettingsProps, +} from '../../typings'; +import type {OverlayControlItem, PreparedCopyItemOptions} from '../OverlayControls/OverlayControls'; + +import {registerManager} from './registration'; + +interface DashKitGeneralProps { + config: Config; + editMode: boolean; + draggableHandleClassName?: string; + overlayControls?: Record | null; + overlayMenuItems?: MenuItem[] | null; +} + +interface DashKitDefaultProps { + defaultGlobalParams: GlobalParams; + globalParams: GlobalParams; + itemsStateAndParams: ItemsStateAndParams; + settings: SettingsProps; + context: ContextProps; + noOverlay: boolean; + focusable?: boolean; + groups?: DashKitGroup[]; + + onItemEdit: (item: ConfigItem) => void; + onChange: (data: { + config: Config; + itemsStateAndParams: ItemsStateAndParams; + groups?: DashKitGroup[]; + }) => void; + + onDrop: (dropProps: ItemDropProps) => void; + + onItemMountChange?: (item: ConfigItem, state: {isAsync: boolean; isMounted: boolean}) => void; + onItemRender?: (item: ConfigItem) => void; + + getPreparedCopyItemOptions?: (options: PreparedCopyItemOptions) => PreparedCopyItemOptions; + onCopyFulfill?: (error: null | Error, data?: PreparedCopyItemOptions) => void; + + onItemFocus?: (item: ConfigItem) => void; + onItemBlur?: (item: ConfigItem) => void; + + onDragStart?: ItemManipulationCallback; + onDrag?: ItemManipulationCallback; + onDragStop?: ItemManipulationCallback; + onResizeStart?: ItemManipulationCallback; + onResize?: ItemManipulationCallback; + onResizeStop?: ItemManipulationCallback; +} + +export interface DashKitProps extends DashKitGeneralProps, Partial {} + +type DashKitInnerProps = DashKitGeneralProps & DashKitDefaultProps; + +/** + * @internal Not part of the public API. Do not use in production code. + * Module-scoped symbol — not accessible to external consumers holding a ref, + * preventing synthetic event injection via instance._emit. + */ +export const _emitSymbol = Symbol('DashKit._emit'); + +export interface DashKitLayoutRef { + getItemsMeta(): Array>; + reloadItems(options?: {targetIds?: string[]; force?: boolean}): void; +} + +export abstract class DashKitBase< + TLayout extends DashKitLayoutRef = DashKitLayoutRef, +> extends React.PureComponent { + static defaultProps: DashKitDefaultProps = { + onItemEdit: noop, + onChange: noop, + onDrop: noop, + defaultGlobalParams: {}, + globalParams: {}, + itemsStateAndParams: {}, + settings: { + autoupdateInterval: 0, + silentLoading: false, + }, + context: {}, + noOverlay: false, + focusable: false, + }; + + static contextType = DashKitDnDContext; + + static registerPlugins(...plugins: Plugin[]) { + plugins.forEach((plugin) => { + registerManager.registerPlugin(plugin); + }); + } + + static reloadPlugins(...plugins: Plugin[]) { + plugins.forEach((plugin) => { + registerManager.reloadPlugin(plugin); + }); + } + + static setSettings(settings: Settings) { + registerManager.setSettings(settings); + } + + metaRef = React.createRef(); + + private _handlers = new Map void>>(); + + getItemsMeta() { + return this.metaRef.current?.getItemsMeta() ?? []; + } + + reloadItems(options?: {targetIds?: string[]; force?: boolean}) { + this.metaRef.current?.reloadItems(options); + } + + /** + * @experimental This API can change in minor releases. + * @param eventName Event name. + * @param handler Event handler. + * @returns Unsubscribe callback. + */ + on(eventName: T, handler: DashKitEventHandler): () => void { + if (!this._handlers.has(eventName)) { + this._handlers.set(eventName, new Set()); + } + this._handlers.get(eventName)?.add(handler); + return () => this._off(eventName, handler); + } + + [_emitSymbol] = (eventName: T, event: DashKitEventMap[T]): void => { + const handlers = this._handlers.get(eventName); + if (!handlers) { + return; + } + for (const handler of handlers) { + try { + (handler as DashKitEventHandler)(event); + } catch (e) { + // eslint-disable-next-line no-console + console.error('DashKit: event handler error', e); + } + } + }; + + private _off(eventName: T, handler: DashKitEventHandler): void { + const handlers = this._handlers.get(eventName); + if (handlers) { + handlers.delete(handler); + if (handlers.size === 0) { + this._handlers.delete(eventName); + } + } + } +} diff --git a/src/components/DashKit/__tests__/events.test.tsx b/src/components/DashKit/__tests__/events.test.tsx index 5f270f3..ac4574e 100644 --- a/src/components/DashKit/__tests__/events.test.tsx +++ b/src/components/DashKit/__tests__/events.test.tsx @@ -104,6 +104,48 @@ describe('DashKit change event — onChange gate', () => { } }); + // Critical: yes. A renderer no-op must not create a saved history transaction. + it('ignores enriched sizing defaults while preserving saved geometry changes', () => { + const onChange = jest.fn(); + const emitDashKitEvent = jest.fn(); + const enriched = {...TEST_CONFIG, layout: [{...TEST_CONFIG.layout[0], minW: 1, minH: 1}]}; + emitDashKitChangeEvent({ + config: enriched, + newConfig: TEST_CONFIG, + onChange, + emitDashKitEvent, + }); + expect(emitDashKitEvent).not.toHaveBeenCalled(); + expect(onChange).not.toHaveBeenCalled(); + emitDashKitChangeEvent({ + config: enriched, + newConfig: NEXT_CONFIG, + onChange, + emitDashKitEvent, + }); + expect(emitDashKitEvent).toHaveBeenCalledTimes(1); + expect(emitDashKitEvent.mock.calls[0][1].patches).toEqual(NEXT_LAYOUT); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it('preserves layout additions, removals and order changes', () => { + const second = {...TEST_CONFIG.layout[0], i: 'item2', x: 4}; + for (const [previous, next] of [ + [TEST_CONFIG, {...TEST_CONFIG, layout: [...TEST_CONFIG.layout, second]}], + [TEST_CONFIG, {...TEST_CONFIG, layout: []}], + [ + {...TEST_CONFIG, layout: [...TEST_CONFIG.layout, second]}, + {...TEST_CONFIG, layout: [second, ...TEST_CONFIG.layout]}, + ], + ] as Array<[Config, Config]>) { + const onChange = jest.fn(); + const emitDashKitEvent = jest.fn(); + emitDashKitChangeEvent({config: previous, newConfig: next, onChange, emitDashKitEvent}); + expect(emitDashKitEvent).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith({config: next}); + } + }); + it('same layout: event and onChange do not fire', () => { const onChange = jest.fn(); const emitDashKitEvent = jest.fn(); diff --git a/src/components/DashKit/__tests__/flat-contract.test.tsx b/src/components/DashKit/__tests__/flat-contract.test.tsx new file mode 100644 index 0000000..8e4b20b --- /dev/null +++ b/src/components/DashKit/__tests__/flat-contract.test.tsx @@ -0,0 +1,588 @@ +/** @jest-environment jsdom */ +jest.mock('@gravity-ui/icons', () => ({Ellipsis: 'ellipsis', Gear: 'gear', Xmark: 'xmark'})); +jest.mock('@gravity-ui/uikit', () => { + const ReactMock = require('react'); + return { + Button: ({children}: {children?: unknown}) => + ReactMock.createElement('button', null, children), + DropdownMenu: () => null, + Icon: () => null, + }; +}); + +import assert from 'node:assert/strict'; + +import React from 'react'; + +import {act, fireEvent, render} from '@testing-library/react'; + +import {DashKitContext} from '../../../context'; +import type {Config} from '../../../shared'; +import type {DashKitEventMap, DashKitGroup, PluginWidgetProps} from '../../../typings'; +import {DashKit} from '../DashKit'; + +const type = 'flat-contract-widget'; +const config: Config = { + salt: 'flat-contract', + counter: 1, + aliases: {}, + connections: [], + items: ['a', 'b'].map((id) => ({ + id, + type, + namespace: 'default', + data: {text: id}, + defaults: {value: ['initial']}, + })), + layout: [ + {i: 'a', x: 0, y: 0, w: 2, h: 2}, + {i: 'b', x: 6, y: 0, w: 2, h: 2}, + ], +}; +const groups: DashKitGroup[] = [ + { + gridProperties: () => ({ + cols: 12, + rowHeight: 20, + margin: [0, 0], + containerPadding: [0, 0], + compactType: null, + }), + }, +]; +let identity = 0; +const reload = jest.fn(); +class Widget extends React.Component { + static contextType = DashKitContext; + declare context: React.ContextType; + identity = ++identity; + render() { + const entry = this.props.layout.find(({i}) => i === this.props.id); + assert(entry); + return ( +
+ {this.props.data.text as string} + + + +
+ ); + } + getMeta = () => Promise.resolve({id: this.props.id, identity: this.identity}); + reload = (options: unknown) => reload(this.props.id, options); +} +const geometry = (container: HTMLElement, id: string) => { + const plugin = container.querySelector(`[data-widget="${id}"]`); + assert(plugin); + const el = plugin.closest('[data-qa="dashkit-grid-item"]'); + assert(el); + return Object.fromEntries( + ['x', 'y', 'w', 'h'].map((key) => [ + key, + Number(el.getAttribute(`data-dashkit-grid-${key}`)), + ]), + ); +}; +const rects = (layout: Config['layout']) => layout.map(({i, x, y, w, h}) => ({i, x, y, w, h})); + +// Critical: yes. Saved edits, parameter identity and restore must survive either renderer. +// Only UI chrome is mocked; layout components, context, Item and plugin refs are real. +const LAYOUT_ENGINES = [['legacy', DashKit]] as const; +describe.each(LAYOUT_ENGINES)('[%s] common flat DashKit contract', (_engine, Component) => { + beforeAll(() => { + Component.reloadPlugins({ + type, + defaultLayout: {w: 2, h: 2, minW: 1, minH: 1}, + renderer: (props, ref) => , + }); + Object.defineProperty(HTMLElement.prototype, 'clientWidth', { + configurable: true, + get: () => 600, + }); + Object.defineProperty(HTMLElement.prototype, 'offsetParent', { + configurable: true, + get() { + return this.parentElement; + }, + }); + Object.defineProperty(HTMLElement.prototype, 'clientHeight', { + configurable: true, + get: () => 400, + }); + jest.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function ( + this: HTMLElement, + ) { + const entry = this.matches('[data-dashkit-id]') + ? this.querySelector('[data-qa="dashkit-grid-item"]') + : this.closest('[data-qa="dashkit-grid-item"]'); + const x = + this.style.position === 'fixed' + ? parseFloat(this.style.left) || 0 + : Number(entry?.dataset.dashkitGridX || 0) * 50; + const y = + this.style.position === 'fixed' + ? parseFloat(this.style.top) || 0 + : Number(entry?.dataset.dashkitGridY || 0) * 20; + const width = entry ? Number(entry.dataset.dashkitGridW) * 50 : 600; + const height = entry ? Number(entry.dataset.dashkitGridH) * 20 : 400; + return { + x, + y, + width, + height, + top: y, + left: x, + right: x + width, + bottom: y + height, + toJSON: () => ({}), + }; + }); + }); + afterAll(() => jest.restoreAllMocks()); + beforeEach(() => reload.mockClear()); + + const drag = async (container: HTMLElement, id: string, dx: number) => { + const plugin = container.querySelector(`[data-widget="${id}"]`); + assert(plugin); + const target = plugin.closest('[data-qa="dashkit-grid-item"]'); + assert(target); + const x = Number(target.dataset.dashkitGridX) * 50 + 25; + fireEvent.mouseDown(target, {clientX: x, clientY: 10, buttons: 1}); + fireEvent.mouseMove(document, {clientX: x + 5, clientY: 10, buttons: 1}); + fireEvent.mouseMove(document, {clientX: x + dx, clientY: 10, buttons: 1}); + fireEvent.mouseUp(document, {clientX: x + dx, clientY: 10}); + await act(async () => { + await Promise.resolve(); + }); + }; + + test('mounted drag emits one public change and controlled restore applies without echo', async () => { + const onChange = jest.fn(); + const events: DashKitEventMap['change'][] = []; + const view = render( + { + instance?.on('change', (event) => events.push(event)); + }} + />, + ); + const plugin = view.container.querySelector('[data-widget="a"]'); + await drag(view.container, 'a', 50); + expect(events).toHaveLength(1); + expect(rects(events[0].previousLayout)).toEqual(config.layout); + expect(events[0].patches).toEqual([{i: 'a', x: 1, y: 0, w: 2, h: 2}]); + expect(onChange).toHaveBeenCalledTimes(1); + expect(rects(onChange.mock.calls[0][0].config.layout)).toEqual([ + {...config.layout[0], x: 1}, + config.layout[1], + ]); + expect(geometry(view.container, 'a')).toEqual({x: 1, y: 0, w: 2, h: 2}); + view.rerender( + ({...entry}))}} + groups={groups} + editMode + onChange={onChange} + />, + ); + expect(geometry(view.container, 'a')).toEqual({x: 0, y: 0, w: 2, h: 2}); + expect(view.container.querySelector('[data-widget="a"]')).toBe(plugin); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + test('a mounted no-op gesture emits neither change nor onChange', async () => { + const onChange = jest.fn(); + const event = jest.fn(); + const view = render( + { + instance?.on('change', event); + }} + />, + ); + await drag(view.container, 'a', 0); + expect(event).not.toHaveBeenCalled(); + expect(onChange).not.toHaveBeenCalled(); + expect(geometry(view.container, 'a')).toEqual({x: 0, y: 0, w: 2, h: 2}); + }); + + test('preventDefault advances the incremental baseline used by later gestures and params', async () => { + const onChange = jest.fn(); + const events: DashKitEventMap['change'][] = []; + const view = render( + { + instance?.on('change', (event) => { + events.push(event); + event.preventDefault(); + }); + }} + />, + ); + await drag(view.container, 'a', 50); + await drag(view.container, 'b', 50); + expect(events).toHaveLength(2); + expect(events.every((event) => event.defaultPrevented)).toBe(true); + expect(events[0].patches).toEqual([{i: 'a', x: 1, y: 0, w: 2, h: 2}]); + expect(events[1].patches).toEqual([{i: 'b', x: 7, y: 0, w: 2, h: 2}]); + expect(rects(events[1].previousLayout)).toEqual([ + {...config.layout[0], x: 1}, + config.layout[1], + ]); + expect(onChange).not.toHaveBeenCalled(); + fireEvent.click(view.getByText('Params b')); + expect(onChange).toHaveBeenCalledTimes(1); + expect(rects(onChange.mock.calls[0][0].config.layout)).toEqual([ + {...config.layout[0], x: 1}, + {...config.layout[1], x: 7}, + ]); + expect(onChange.mock.calls[0][0].itemsStateAndParams).toMatchObject({ + b: {state: {clicked: true}, params: {value: ['next']}}, + }); + }); + + test('unsubscribe and throwing subscribers preserve later handlers and normal onChange', async () => { + const removed = jest.fn(); + const received = jest.fn(); + const onChange = jest.fn(); + const error = jest.spyOn(console, 'error').mockImplementation(() => undefined); + const view = render( + { + if (instance) { + instance.on('change', removed)(); + instance.on('change', () => { + throw new Error('subscriber failure'); + }); + instance.on('change', received); + } + }} + />, + ); + await drag(view.container, 'a', 50); + expect(removed).not.toHaveBeenCalled(); + expect(received).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledTimes(1); + expect(error).toHaveBeenCalled(); + error.mockRestore(); + }); + + test('external add, data update, reorder and removal retain widget identity and live refs', async () => { + let instance: DashKit | null = null; + const current = () => { + assert(instance); + return instance; + }; + const ref = (value: typeof instance) => { + instance = value; + }; + const onChange = jest.fn(); + const mounted = jest.fn(); + const ready = jest.fn(); + const view = render( + , + ); + const original = view.container.querySelector('[data-widget="a"]'); + const originalMeta = await Promise.all(current().getItemsMeta()); + const reordered = { + ...config, + items: [...config.items].reverse(), + layout: [...config.layout].reverse(), + }; + view.rerender( + , + ); + expect(await Promise.all(current().getItemsMeta())).toEqual( + expect.arrayContaining(originalMeta), + ); + expect(current().getItemsMeta()).toHaveLength(2); + expect(view.container.querySelector('[data-widget="a"]')).toBe(original); + const added = { + ...config, + items: [...config.items, {...config.items[0], id: 'c', data: {text: 'added'}}], + layout: [...config.layout, {...config.layout[0], i: 'c', y: 4}], + }; + view.rerender( + , + ); + const addedPlugin = view.getByText('added'); + const addedMeta = (await Promise.all(current().getItemsMeta())).find( + (entry) => entry.id === 'c', + ); + expect(current().getItemsMeta()).toHaveLength(3); + expect(view.container.querySelector('[data-widget="a"]')).toBe(original); + const retained = { + ...added, + items: [{...added.items[2], data: {text: 'updated'}}], + layout: [{...added.layout[2], x: 3}], + }; + view.rerender( + , + ); + expect(view.getByText('updated')).toBe(addedPlugin); + expect(geometry(view.container, 'c')).toEqual({x: 3, y: 4, w: 2, h: 2}); + expect(await Promise.all(current().getItemsMeta())).toEqual([addedMeta]); + current().reloadItems({targetIds: ['missing'], force: true}); + expect(reload).not.toHaveBeenCalled(); + current().reloadItems({targetIds: ['c'], force: true}); + expect(reload).toHaveBeenCalledWith('c', {silentLoading: false, noVeil: true}); + expect(mounted.mock.calls.filter(([, state]) => state.isMounted)).toHaveLength(2); + expect(ready).toHaveBeenCalledTimes(2); + expect(onChange).not.toHaveBeenCalled(); + view.unmount(); + expect(instance).toBeNull(); + }); + + test('controlled unrendered group membership survives another widget save and later reveal', async () => { + const renderGroup: DashKitGroup['render'] = (id, content) => ( +
+ {content} +
+ ); + const left = {...groups[0], id: 'left', render: renderGroup}; + const inactive = {...groups[0], id: 'inactive', render: renderGroup}; + const initial = { + ...config, + layout: config.layout.map((entry) => ({...entry, parent: 'left'})), + }; + const hidden = { + ...initial, + layout: initial.layout.map((entry) => + entry.i === 'a' ? {...entry, parent: 'inactive'} : entry, + ), + }; + const before = JSON.stringify({initial, hidden}); + const onChange = jest.fn(); + const view = render( + , + ); + expect(view.container.querySelectorAll('[data-widget]')).toHaveLength(2); + view.rerender(); + expect(view.container.querySelector('[data-widget="a"]')).toBeNull(); + expect(view.container.querySelectorAll('[data-widget]')).toHaveLength(1); + expect( + view.container.querySelector('[data-group="left"] [data-widget="b"]'), + ).not.toBeNull(); + expect(onChange).not.toHaveBeenCalled(); + + await drag(view.container, 'b', 50); + expect(onChange).toHaveBeenCalledTimes(1); + const saved: Config = onChange.mock.calls[0][0].config; + expect(saved.layout).toEqual([hidden.layout[0], {...hidden.layout[1], x: 7}]); + expect(view.container.querySelector('[data-widget="a"]')).toBeNull(); + expect(JSON.stringify({initial, hidden})).toBe(before); + + view.rerender( + , + ); + expect(view.container.querySelectorAll('[data-widget]')).toHaveLength(2); + expect( + view.container.querySelector('[data-group="inactive"] [data-widget="a"]'), + ).not.toBeNull(); + expect( + view.container.querySelector('[data-group="left"] [data-widget="b"]'), + ).not.toBeNull(); + expect(geometry(view.container, 'a')).toEqual({x: 0, y: 0, w: 2, h: 2}); + expect(geometry(view.container, 'b')).toEqual({x: 7, y: 0, w: 2, h: 2}); + expect(onChange).toHaveBeenCalledTimes(1); + expect(JSON.stringify({initial, hidden})).toBe(before); + }); + + test('view to edit transition retains widget identity and one metadata ref per item without echo', async () => { + let instance: DashKit | null = null; + const current = () => { + assert(instance); + return instance; + }; + const ref = (value: typeof instance) => { + instance = value; + }; + const onChange = jest.fn(); + const view = render( + , + ); + const plugin = view.container.querySelector('[data-widget="a"]'); + const meta = await Promise.all(current().getItemsMeta()); + view.rerender( + , + ); + expect(view.container.querySelector('[data-widget="a"]')).toBe(plugin); + expect(view.container.querySelectorAll('[data-qa="dashkit-grid-item"]')).toHaveLength(2); + expect(await Promise.all(current().getItemsMeta())).toEqual(meta); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('visual autoheight survives a saved move and reset keeps the saved height', async () => { + const onChange = jest.fn(); + const view = render( + , + ); + fireEvent.click(view.getByText('Grow a')); + expect(geometry(view.container, 'a').h).toBe(4); + expect(onChange).not.toHaveBeenCalled(); + await drag(view.container, 'a', 50); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0].config.layout[0]).toMatchObject({x: 1, h: 2}); + expect(geometry(view.container, 'a').h).toBe(4); + fireEvent.click(view.getByText('Reset a')); + expect(geometry(view.container, 'a').h).toBe(2); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + // Critical: yes. Mounting is not readiness; stale/unmounted async completions must not release loading. + test('offscreen async content reports readiness only for the latest live load', () => { + const completions: Array<() => void> = []; + class AsyncWidget extends React.Component { + componentDidMount() { + completions.push(this.props.onBeforeLoad()); + } + componentDidUpdate(previous: PluginWidgetProps) { + if (previous.data !== this.props.data) { + completions.push(this.props.onBeforeLoad()); + } + } + render() { + return {this.props.data.text as string}; + } + } + const asyncType = 'flat-contract-async'; + Component.reloadPlugins({ + type: asyncType, + renderer: (props, ref) => , + }); + const offscreen = { + ...config, + items: [{...config.items[0], type: asyncType}], + layout: [{...config.layout[0], y: 4000}], + }; + const mounted = jest.fn(); + const ready = jest.fn(); + const view = render( + , + ); + expect(mounted).toHaveBeenCalledWith(offscreen.items[0], { + isAsync: true, + isMounted: true, + }); + expect(ready).not.toHaveBeenCalled(); + const latest = {...offscreen.items[0], data: {text: 'latest'}}; + view.rerender( + , + ); + act(() => completions[0]()); + expect(ready).not.toHaveBeenCalled(); + act(() => completions[1]()); + expect(ready).toHaveBeenCalledTimes(1); + expect(ready).toHaveBeenCalledWith(latest); + view.unmount(); + act(() => completions[1]()); + expect(ready).toHaveBeenCalledTimes(1); + expect(mounted.mock.calls.at(-1)[1].isMounted).toBe(false); + }); + + test('flat statics add, edit, promote and remove without mutating the caller document', () => { + const before = JSON.stringify(config); + const added = Component.setItem({ + config, + item: {type, data: {text: 'added'}, layout: {x: 3, y: 4, w: 2, h: 2}}, + groups, + }); + const item = added.items[2]; + expect(added.items).toHaveLength(3); + expect(added.layout[2]).toMatchObject({i: item.id, x: 3, y: 4, w: 2, h: 2}); + const promoted = Component.setItem({ + config: added, + item: {...item, data: {text: 'updated'}}, + options: {useGlobalItems: true}, + groups, + }); + expect(promoted.globalItems).toEqual([{...item, data: {text: 'updated'}}]); + expect(promoted.items).toEqual(config.items); + expect(promoted.layout).toEqual(added.layout); + const removed = Component.removeItem({ + config: promoted, + id: item.id, + itemsStateAndParams: {[item.id]: {state: {clicked: true}}}, + }); + expect(removed.config.items).toEqual(config.items); + expect(removed.config.globalItems).toEqual([]); + expect(rects(removed.config.layout)).toEqual(config.layout); + expect(removed.itemsStateAndParams).toEqual({__meta__: {version: 2, queue: []}}); + expect(JSON.stringify(config)).toBe(before); + }); +}); diff --git a/src/components/DashKit/registration.ts b/src/components/DashKit/registration.ts new file mode 100644 index 0000000..2523461 --- /dev/null +++ b/src/components/DashKit/registration.ts @@ -0,0 +1,3 @@ +import {RegisterManager} from '../../utils/register-manager'; + +export const registerManager = new RegisterManager(); diff --git a/src/components/Item/Item.tsx b/src/components/Item/Item.tsx index 352b6a2..f3bb148 100644 --- a/src/components/Item/Item.tsx +++ b/src/components/Item/Item.tsx @@ -24,6 +24,8 @@ const Item: React.FC = ({ const itemRef = React.useRef(item); const onItemRenderRef = React.useRef(onItemRender); const onItemMountChangeRef = React.useRef(onItemMountChange); + const mountedRef = React.useRef(true); + const loadGenerationRef = React.useRef(0); itemRef.current = item; const {type} = item; @@ -33,6 +35,7 @@ const Item: React.FC = ({ const isRegisteredType = registerManager.check(type); React.useLayoutEffect(() => { + mountedRef.current = true; if (isRegisteredType && !isPlaceholder) { onItemMountChangeRef.current?.(itemRef.current, { isAsync: isAsyncItemRef.current, @@ -44,6 +47,7 @@ const Item: React.FC = ({ } return () => { + mountedRef.current = false; onItemMountChangeRef.current?.(itemRef.current, { isAsync: isAsyncItemRef.current, isMounted: false, @@ -60,8 +64,12 @@ const Item: React.FC = ({ const onBeforeLoad = React.useCallback(() => { isAsyncItemRef.current = true; - - return onLoad; + const generation = ++loadGenerationRef.current; + return () => { + if (mountedRef.current && generation === loadGenerationRef.current) { + onLoad(); + } + }; }, [onLoad]); const itemRendererProps = React.useMemo(() => { diff --git a/src/components/Item/__tests__/readiness.test.tsx b/src/components/Item/__tests__/readiness.test.tsx new file mode 100644 index 0000000..cd553a6 --- /dev/null +++ b/src/components/Item/__tests__/readiness.test.tsx @@ -0,0 +1,101 @@ +/** @jest-environment jsdom */ +import React from 'react'; + +import {act, render} from '@testing-library/react'; + +import {DashKitContext} from '../../../context/DashKitContext'; +import type {DashKitCtxShape} from '../../../context/DashKitContext'; +import type {ConfigItem} from '../../../shared'; +import type {PluginWidgetProps} from '../../../typings'; +import {RegisterManager} from '../../../utils/register-manager'; +import Item from '../Item'; + +// Critical: yes. Stale readiness can export unfinished content or signal a removed widget as ready. +test('only the latest live async load marks the current item ready', () => { + const completions: Array<() => void> = []; + class AsyncPlugin extends React.Component { + componentDidMount() { + completions.push(this.props.onBeforeLoad()); + } + componentDidUpdate(previous: PluginWidgetProps) { + if (previous.data !== this.props.data) { + completions.push(this.props.onBeforeLoad()); + } + } + render() { + return {this.props.data.text as string}; + } + } + const registerManager = new RegisterManager(); + registerManager.registerPlugin({ + type: 'async-readiness', + renderer: (props) => , + }); + const item: ConfigItem = { + id: 'async', + type: 'async-readiness', + namespace: 'default', + data: {text: 'initial'}, + }; + const context: DashKitCtxShape = { + config: {salt: 'test', counter: 1, items: [item], layout: [], aliases: {}, connections: []}, + editMode: false, + context: {}, + settings: {autoupdateInterval: 0, silentLoading: false}, + globalParams: {}, + registerManager, + forwardedMetaRef: null, + externalLayoutRevision: 0, + configItems: [item], + layout: [], + layoutChange: jest.fn(), + temporaryLayout: null, + memorizeOriginalLayout: jest.fn(), + revertToOriginalLayout: jest.fn(), + itemsState: {async: {}}, + itemsParams: {async: {}}, + onItemStateAndParamsChange: jest.fn(), + getItemsMeta: jest.fn(), + reloadItems: jest.fn(), + onDrop: jest.fn(), + onDropDragOver: jest.fn(), + outerDnDEnable: false, + dragOverPlugin: null, + }; + const mounted = jest.fn(); + const ready = jest.fn(); + const adjustWidgetLayout = jest.fn(); + const fixture = (current: ConfigItem) => ( + + + + ); + const view = render(fixture(item)); + expect(view.getByText('initial')).toBeTruthy(); + expect(completions).toHaveLength(1); + expect(mounted).toHaveBeenCalledWith(item, {isAsync: true, isMounted: true}); + expect(ready).not.toHaveBeenCalled(); + + const latest = {...item, data: {text: 'latest'}}; + view.rerender(fixture(latest)); + expect(view.getByText('latest')).toBeTruthy(); + expect(completions).toHaveLength(2); + act(() => completions[0]()); + expect(ready).not.toHaveBeenCalled(); + act(() => completions[1]()); + expect(ready).toHaveBeenCalledTimes(1); + expect(ready).toHaveBeenLastCalledWith(latest); + + view.unmount(); + act(() => completions[1]()); + expect(ready).toHaveBeenCalledTimes(1); + expect(mounted).toHaveBeenLastCalledWith(latest, {isAsync: true, isMounted: false}); +}); diff --git a/src/plugins/Text/Text.tsx b/src/plugins/Text/Text.tsx index a53f160..3155945 100644 --- a/src/plugins/Text/Text.tsx +++ b/src/plugins/Text/Text.tsx @@ -56,6 +56,7 @@ export class PluginText extends React.PureComponent { + if (!this._isUnmounted && generation === this._requestGeneration) { + onLoadComplete(); + } + }, + ); } catch { - if (this._isUnmounted) { + if (this._isUnmounted || generation !== this._requestGeneration) { return; } - this.setState({status: LoadStatus.Fail}); + this.setState({status: LoadStatus.Fail}, () => { + if (!this._isUnmounted && generation === this._requestGeneration) { + onLoadComplete(); + } + }); } - onLoadComplete(); } private onRetryClick = () => { diff --git a/src/plugins/Text/__tests__/async-generation.test.tsx b/src/plugins/Text/__tests__/async-generation.test.tsx new file mode 100644 index 0000000..6d6d3a9 --- /dev/null +++ b/src/plugins/Text/__tests__/async-generation.test.tsx @@ -0,0 +1,108 @@ +/** @jest-environment jsdom */ +jest.mock('@gravity-ui/uikit', () => { + const ReactMock = require('react'); + return { + Loader: () => ReactMock.createElement('span', null, 'loading'), + Button: ({children}: {children?: unknown}) => + ReactMock.createElement('button', null, children), + }; +}); + +import React from 'react'; + +import {act, render} from '@testing-library/react'; + +import {PluginText} from '../Text'; +import type {PluginTextProps} from '../Text'; + +function pending() { + let resolve!: (result: {result: string}) => void; + let reject!: (error: Error) => void; + const promise = new Promise<{result: string}>((yes, no) => { + resolve = yes; + reject = no; + }); + return {promise, resolve, reject}; +} + +// Critical: yes. Stale markdown must not overwrite the latest preview/export DOM or mark it ready. +test.each(['success', 'failure'] as const)( + 'ignores stale %s after a newer request completes', + async (outcome) => { + const old = pending(); + const next = pending(); + const oldComplete = jest.fn(); + const nextComplete = jest.fn(); + const onBeforeLoad = jest + .fn() + .mockReturnValueOnce(oldComplete) + .mockReturnValueOnce(nextComplete); + const props: PluginTextProps = { + id: 'text', + editMode: false, + params: {}, + state: {}, + onStateAndParamsChange: jest.fn(), + onBeforeLoad, + onBeforeRender: jest.fn(), + data: {text: 'old'}, + defaults: {}, + namespace: 'default', + settings: {autoupdateInterval: 0, silentLoading: false}, + context: {}, + layout: [], + gridLayout: {}, + adjustWidgetLayout: jest.fn(), + apiHandler: ({text}) => (text === 'old' ? old.promise : next.promise), + }; + const {rerender, getByText, queryByText} = render(); + rerender(); + await act(async () => next.resolve({result: '

latest content

'})); + expect(getByText('latest content')).toBeTruthy(); + expect(nextComplete).toHaveBeenCalledTimes(1); + await act(async () => { + if (outcome === 'success') { + old.resolve({result: '

stale content

'}); + } else { + old.reject(new Error('stale failure')); + } + }); + expect(getByText('latest content')).toBeTruthy(); + expect(queryByText('stale content')).toBeNull(); + expect(oldComplete).not.toHaveBeenCalled(); + expect(nextComplete).toHaveBeenCalledTimes(1); + }, +); + +test('completion observes committed latest DOM and unmounted requests never complete', async () => { + const response = pending(); + const complete = jest.fn(() => expect(document.body.textContent).toContain('committed')); + const props: PluginTextProps = { + id: 'text', + editMode: false, + params: {}, + state: {}, + onStateAndParamsChange: jest.fn(), + onBeforeLoad: () => complete, + onBeforeRender: jest.fn(), + data: {text: 'load'}, + defaults: {}, + namespace: 'default', + settings: {autoupdateInterval: 0, silentLoading: false}, + context: {}, + layout: [], + gridLayout: {}, + adjustWidgetLayout: jest.fn(), + apiHandler: () => response.promise, + }; + const view = render(); + await act(async () => response.resolve({result: '

committed

'})); + expect(complete).toHaveBeenCalledTimes(1); + const stale = pending(); + view.rerender( + stale.promise} />, + ); + view.unmount(); + await act(async () => stale.resolve({result: '

removed

'})); + expect(complete).toHaveBeenCalledTimes(1); +}); diff --git a/src/utils/events.ts b/src/utils/events.ts index b75f550..c55023e 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -8,6 +8,7 @@ import type { DashKitLayoutPatch, } from '../typings'; +import {convertEnrichedLayoutToConfigLayout} from './enrichLayoutWithDefaults'; import {getLayoutPatches} from './get-layout-patches'; // Only fires when layout changes; other config mutations (item ordering, counter) do not trigger this event. @@ -22,7 +23,12 @@ export function emitDashKitChangeEvent({ onChange: (data: {config: Config}) => void; emitDashKitEvent: (eventName: T, event: DashKitEventMap[T]) => void; }) { - if (!isEqual(newConfig.layout, config.layout)) { + if ( + !isEqual( + convertEnrichedLayoutToConfigLayout(newConfig.layout), + convertEnrichedLayoutToConfigLayout(config.layout), + ) + ) { let defaultPrevented = false; let cachedPatches: DashKitLayoutPatch[] | undefined; const event: DashKitChangeEvent = {