From 8d00b2b2689083f3c20652192b8f82c83cf419f9 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:11:41 +0100 Subject: [PATCH 01/15] feat: revamp select --- package-lock.json | 461 ++++++++++++++++++++++++ package.json | 3 +- src/components/pickers/DevicePicker.tsx | 130 ++++--- src/consts.ts | 19 + src/types.ts | 9 + 5 files changed, 555 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5576e600f..fcb32529d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "react-i18next": "^16.4.0", "react-image": "^4.1.0", "react-router": "^7.10.1", + "react-select": "^5.10.2", "react-virtuoso": "^4.17.0", "reagraph": "^4.30.7", "store2": "^2.14.4", @@ -765,6 +766,148 @@ "react-dom": ">16.8.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", @@ -2795,6 +2938,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", @@ -2826,6 +2976,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", @@ -3272,6 +3432,22 @@ "node": ">=4" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3401,6 +3577,16 @@ "ieee754": "^1.2.1" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camera-controls": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", @@ -3530,6 +3716,33 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -3902,6 +4115,17 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/draco3d": { "version": "1.5.7", "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", @@ -3974,6 +4198,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -4034,6 +4268,19 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -4124,6 +4371,13 @@ "dev": true, "license": "MIT" }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true, + "license": "MIT" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -4359,6 +4613,23 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/hold-event": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/hold-event/-/hold-event-1.1.2.tgz", @@ -4508,6 +4779,23 @@ "dev": true, "license": "MIT" }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -4535,6 +4823,13 @@ "node": ">=12" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -4753,6 +5048,13 @@ "node": ">=6" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5050,6 +5352,13 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -5057,6 +5366,19 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -5153,6 +5475,13 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "dev": true, + "license": "MIT" + }, "node_modules/meshline": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", @@ -5357,6 +5686,38 @@ "mnemonist": "^0.39.2" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -5411,6 +5772,16 @@ "dev": true, "license": "ISC" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -5535,6 +5906,25 @@ "lie": "^3.0.2" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5733,6 +6123,45 @@ } } }, + "node_modules/react-select": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", + "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/react-use-measure": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", @@ -5925,6 +6354,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/rollup": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", @@ -6325,6 +6764,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6740,6 +7186,21 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", diff --git a/package.json b/package.json index 79a2e0a6f..37a4b6695 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "react-i18next": "^16.4.0", "react-image": "^4.1.0", "react-router": "^7.10.1", + "react-select": "^5.10.2", "react-virtuoso": "^4.17.0", "reagraph": "^4.30.7", "store2": "^2.14.4", @@ -102,4 +103,4 @@ "type": "github", "url": "https://github.com/sponsors/Nerivec" } -} \ No newline at end of file +} diff --git a/src/components/pickers/DevicePicker.tsx b/src/components/pickers/DevicePicker.tsx index 6feb19a96..e22045bb8 100644 --- a/src/components/pickers/DevicePicker.tsx +++ b/src/components/pickers/DevicePicker.tsx @@ -1,96 +1,94 @@ -import { type ChangeEvent, type JSX, memo, type SelectHTMLAttributes, useEffect, useMemo, useState } from "react"; +import { memo, useMemo } from "react"; import { useTranslation } from "react-i18next"; +import Select, { type SingleValue } from "react-select"; +import { REACT_SELECT_DEFAULT_CLASSNAMES } from "../../consts.js"; import type { AppState } from "../../store.js"; -import type { Device, Group } from "../../types.js"; -import SelectField from "../form-fields/SelectField.js"; +import type { BaseGroupedOption, BaseSelectOption, Device, Group } from "../../types.js"; -interface DevicePickerProps extends Omit, "onChange"> { +interface DevicePickerProps { devices: AppState["devices"][number]; value: string | number; label?: string; + detail?: string; + required?: boolean; + disabled?: boolean; groups?: Group[]; onChange(device?: Device | Group): void; } -const DevicePicker = memo(({ devices, value, label, onChange, groups = [], ...rest }: DevicePickerProps) => { +const DevicePicker = memo(({ devices, value, label, detail, required, disabled, onChange, groups = [] }: DevicePickerProps) => { const { t } = useTranslation("common"); - const [selectedName, setSelectedName] = useState(""); - useEffect(() => { - if (typeof value === "string") { - const device = devices.find((device) => device.ieee_address === value); + const onSelectHandler = (option: SingleValue): void => { + if (option) { + if (option.value.startsWith("0x") /* ieee */) { + onChange(devices.find((device) => device.ieee_address === option.value)); + } else { + const selectedId = Number.parseInt(option.value, 10); - setSelectedName(device?.friendly_name ?? ""); - } else { - const group = groups.find((g) => value === g.id); - - setSelectedName(group?.friendly_name ?? ""); - } - }, [value, devices, groups]); - - const onSelectHandler = (e: ChangeEvent): void => { - const { value: selectedValue } = e.target; - - if (selectedValue.startsWith("0x") /* ieee */) { - onChange(devices.find((device) => device.ieee_address === selectedValue)); - } else { - const selectedId = Number.parseInt(selectedValue, 10); - - onChange(groups.find((g) => selectedId === g.id)); + onChange(groups.find((g) => selectedId === g.id)); + } } }; const options = useMemo(() => { - const options: JSX.Element[] = []; - const devicesOptions = devices - .map((device) => ( - - )) - .sort((elA, elB) => elA.key!.localeCompare(elB.key!)); + // should always be: [0] devices, [1] groups + const options: BaseGroupedOption[] = []; + const devicesOptions: BaseSelectOption[] = devices + .map((device) => ({ + value: device.ieee_address, + label: `${device.friendly_name} ${device.definition?.model ? `(${device.definition?.model})` : ""}`, + })) + .sort((elA, elB) => elA.label!.localeCompare(elB.label!)); if (groups?.length) { - const groupOptions = groups - .map((group) => ( - - )) - .sort((elA, elB) => elA.key!.localeCompare(elB.key!)); + const groupOptions: BaseSelectOption[] = groups + .map((group) => ({ value: `${group.id}`, label: group.friendly_name })) + .sort((elA, elB) => elA.label!.localeCompare(elB.label!)); - options.push( - $.groups)}> - {groupOptions} - , - ); - options.push( - $.devices)}> - {devicesOptions} - , - ); + options.push({ label: t(($) => $.devices), options: devicesOptions }); + options.push({ label: t(($) => $.groups), options: groupOptions }); } else { - options.push(...devicesOptions); + options.push({ label: t(($) => $.devices), options: devicesOptions }); } return options; }, [devices, groups, t]); + const selected = useMemo>(() => { + if (value == null || value === "") { + return null; + } + + if (typeof value === "number") { + return options[1]?.options.find((o) => o.value === `${value}`) ?? null; + } + + return options[0]?.options.find((o) => o.value === value) ?? null; + }, [value, options]); + return ( - - - {options} - +
+ {label && ( + + {label} + {required ? " *" : ""} + + )} + $.select_attribute)} + aria-label={label ?? t(($) => $.select_attribute)} + options={options} + value={selected} + isSearchable + isDisabled={disabled} + onChange={(option) => { + if (option != null) { + onChange(option.value, clusterAttributes[option.value]); + } + }} + className="min-w-64" + classNames={REACT_SELECT_DEFAULT_CLASSNAMES} + /> +
); }); diff --git a/src/consts.ts b/src/consts.ts index 41d38bb7f..bbac12547 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -94,8 +94,8 @@ export const REACT_SELECT_DEFAULT_CLASSNAMES = { indicatorSeparator: () => "hidden", menu: () => "!min-w-full !w-fit dropdown-content menu p-0 bg-base-100 text-base-content shadow-md border border-base-300 rounded-box mt-1 !z-2", menuList: () => "max-h-60 overflow-auto", - groupHeading: () => "px-2 py-2 text-xs font-semibold uppercase text-base-content/70", + groupHeading: () => "px-1 py-2 text-xs font-semibold uppercase text-base-content/70", option: ({ isFocused, isSelected }) => - `px-2 py-1 cursor-pointer ${isSelected ? "bg-primary text-primary-content" : isFocused ? "bg-primary/10" : ""}`, + `px-3 py-1 cursor-pointer ${isSelected ? "bg-primary text-primary-content" : isFocused ? "bg-primary/10" : ""}`, noOptionsMessage: () => "px-2 py-1 text-base-content/70", }; From 022679785bcae13c2b9a1098e0f00ed87b9c3feb Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Thu, 11 Dec 2025 17:36:15 +0100 Subject: [PATCH 03/15] fix: revamp cluster single picker --- .../pickers/ClusterSinglePicker.tsx | 94 +++++++++++-------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/src/components/pickers/ClusterSinglePicker.tsx b/src/components/pickers/ClusterSinglePicker.tsx index c82636032..5ab0b707d 100644 --- a/src/components/pickers/ClusterSinglePicker.tsx +++ b/src/components/pickers/ClusterSinglePicker.tsx @@ -1,67 +1,85 @@ -import { type JSX, memo, useMemo } from "react"; +import { memo, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import SelectField, { type SelectFieldProps } from "../form-fields/SelectField.js"; +import Select, { type SingleValue } from "react-select"; +import { REACT_SELECT_DEFAULT_CLASSNAMES } from "../../consts.js"; +import type { BaseGroupedOption, BaseSelectOption } from "../../types.js"; import type { ClusterGroup } from "./index.js"; -export interface ClusterSinglePickerProps extends Omit { +export interface ClusterSinglePickerProps { clusters: Set | ClusterGroup[]; value: string; + label?: string; + required?: boolean; + disabled?: boolean; onChange(cluster: string): void; } -const ClusterSinglePicker = memo(({ clusters, onChange, value, label, disabled, ...rest }: ClusterSinglePickerProps) => { +const ClusterSinglePicker = memo(({ clusters, onChange, value, label, disabled, required }: ClusterSinglePickerProps) => { const { t } = useTranslation(["zigbee", "common"]); - const options = useMemo(() => { - const options: JSX.Element[] = []; + const [options, allOptions] = useMemo(() => { + const options: BaseGroupedOption[] = []; + const allOptions: BaseSelectOption[] = []; if (Array.isArray(clusters)) { for (const group of clusters) { - const groupOptions: JSX.Element[] = []; + const groupOptions: BaseSelectOption[] = []; for (const cluster of group.clusters) { - groupOptions.push( - , - ); + const entry = { value: cluster, label: cluster }; + groupOptions.push(entry); + allOptions.push(entry); } - options.push( - $[group.name])}> - {groupOptions} - , - ); + groupOptions.sort((elA, elB) => elA.label!.localeCompare(elB.label!)); + options.push({ label: t(($) => $[group.name]), options: groupOptions }); } } else { + const groupOptions: BaseSelectOption[] = []; + for (const cluster of clusters) { - options.push( - , - ); + const entry = { value: cluster, label: cluster }; + groupOptions.push(entry); + allOptions.push(entry); } + + groupOptions.sort((elA, elB) => elA.label!.localeCompare(elB.label!)); + options.push({ label: t(($) => $.cluster), options: groupOptions }); } - return options; + return [options, allOptions]; }, [clusters, t]); + const selected = useMemo>( + () => (value == null || value === "" ? null : (allOptions.find((o) => o.value === value) ?? null)), + [value, allOptions], + ); + return ( - onChange(e.target.value)} - disabled={disabled} - className="select validator w-48" - {...rest} - > - - {options} - +
+ {label && ( + + {label} + {required ? " *" : ""} + + )} + $.select_group)} + aria-label={label ?? t(($) => $.select_group)} + options={options} + value={selected} + isSearchable + isDisabled={disabled} + onChange={onSelectHandler} + className="min-w-96" + classNames={REACT_SELECT_DEFAULT_CLASSNAMES} + /> +
); }); From 769147f843c459a0e513c0ba353b07602657ce07 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:06:59 +0100 Subject: [PATCH 07/15] fix: revamp scene picker --- src/components/device-page/RecallRemove.tsx | 9 +-- src/components/pickers/ScenePicker.tsx | 64 ++++++++++----------- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/src/components/device-page/RecallRemove.tsx b/src/components/device-page/RecallRemove.tsx index a4484f6a6..b47c5aca6 100644 --- a/src/components/device-page/RecallRemove.tsx +++ b/src/components/device-page/RecallRemove.tsx @@ -15,13 +15,10 @@ interface RecallRemoveProps { const RecallRemove = memo(({ sourceIdx, target }: RecallRemoveProps) => { const { t } = useTranslation(["scene", "common"]); const [scene, setScene] = useState({ id: 0, name: "Scene 0" }); - const [sceneIsNotSelected, setsceneIsNotSelected] = useState(true); const scenes = useMemo(() => getScenes(target), [target]); const onSceneSelected = useCallback( (sceneId: number) => { - setsceneIsNotSelected(false); - const foundScene = scenes.find((s) => s.id === sceneId); if (foundScene !== undefined) { @@ -70,14 +67,14 @@ const RecallRemove = memo(({ sourceIdx, target }: RecallRemoveProps) => { <>

{t(($) => $.manage_scenes_header)}

- +
- $.remove, { ns: "common" })} diff --git a/src/components/pickers/ScenePicker.tsx b/src/components/pickers/ScenePicker.tsx index d9b65d37d..9f265a92f 100644 --- a/src/components/pickers/ScenePicker.tsx +++ b/src/components/pickers/ScenePicker.tsx @@ -1,44 +1,44 @@ -import { memo } from "react"; +import { memo, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import type { Scene } from "../../types.js"; -import InputField from "../form-fields/InputField.js"; -import SelectField from "../form-fields/SelectField.js"; +import CreatableSelect from "react-select/creatable"; +import { REACT_SELECT_DEFAULT_CLASSNAMES } from "../../consts.js"; +import type { BaseSelectOption, Scene } from "../../types.js"; type ScenePickerProps = { - value?: Scene; scenes: Scene[]; + disabled?: boolean; onSceneSelected: (sceneId: number) => void; }; -const ScenePicker = memo(({ onSceneSelected, scenes = [], value }: ScenePickerProps) => { +const ScenePicker = memo(({ onSceneSelected, scenes = [], disabled }: ScenePickerProps) => { const { t } = useTranslation("scene"); - return scenes.length > 0 ? ( - $.scene_name)} - value={value?.id ?? ""} - onChange={(e) => !e.target.validationMessage && !!e.target.value && onSceneSelected(Number.parseInt(e.target.value, 10))} - > - - {scenes.map((scene) => ( - - ))} - - ) : ( - $.scene_id)} - type="number" - value={value?.id ?? ""} - onChange={(e) => !e.target.validationMessage && !!e.target.value && onSceneSelected(e.target.valueAsNumber)} - min={0} - max={255} - /> + const options = useMemo( + (): BaseSelectOption[] => + scenes + .map((scene) => ({ value: `${scene.id}`, label: `${scene.id}: ${scene.name}` })) + .sort((elA, elB) => elA.label!.localeCompare(elB.label!)), + [scenes], + ); + + return ( +
+ $.scene_name)} + aria-label={t(($) => $.scene_name)} + options={options} + isSearchable + isDisabled={disabled} + onChange={(option) => { + if (option != null) { + onSceneSelected(Number.parseInt(option.value, 10)); + } + }} + className="min-w-64" + classNames={REACT_SELECT_DEFAULT_CLASSNAMES} + /> +
); }); From 98125ed5ab13288e7839f662019a16e0b7eec606 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:11:33 +0100 Subject: [PATCH 08/15] fix: cleanup --- src/components/device-page/AddToGroup.tsx | 2 +- src/components/pickers/GroupPicker.tsx | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/device-page/AddToGroup.tsx b/src/components/device-page/AddToGroup.tsx index f5b1fabd2..634c794e6 100644 --- a/src/components/device-page/AddToGroup.tsx +++ b/src/components/device-page/AddToGroup.tsx @@ -44,7 +44,7 @@ const AddToGroup = memo(({ sourceIdx, device, nonMemberGroups }: AddToGroupProps <>

{t(($) => $.add_to_group)}

- $.group, { ns: "zigbee" })} value={groupId} groups={nonMemberGroups} onChange={onGroupChange} required /> + $.group, { ns: "zigbee" })} groups={nonMemberGroups} onChange={onGroupChange} required /> $.endpoint, { ns: "zigbee" })} values={endpoints} diff --git a/src/components/pickers/GroupPicker.tsx b/src/components/pickers/GroupPicker.tsx index 97254c597..a63e41739 100644 --- a/src/components/pickers/GroupPicker.tsx +++ b/src/components/pickers/GroupPicker.tsx @@ -7,14 +7,13 @@ import type { BaseSelectOption, Group } from "../../types.js"; interface GroupPickerProps { groups: AppState["groups"][number]; - value: string | number; label?: string; required?: boolean; disabled?: boolean; onChange(group?: Group): void; } -const GroupPicker = memo(({ groups, value, label, required, disabled, onChange }: GroupPickerProps) => { +const GroupPicker = memo(({ groups, label, required, disabled, onChange }: GroupPickerProps) => { const { t } = useTranslation("common"); const onSelectHandler = useCallback( @@ -34,11 +33,6 @@ const GroupPicker = memo(({ groups, value, label, required, disabled, onChange } [groups], ); - const selected = useMemo>( - () => (value == null || value === "" ? null : (options.find((o) => o.value === value) ?? null)), - [value, options], - ); - return (
{label && ( @@ -52,7 +46,6 @@ const GroupPicker = memo(({ groups, value, label, required, disabled, onChange } placeholder={t(($) => $.select_group)} aria-label={label ?? t(($) => $.select_group)} options={options} - value={selected} isSearchable isDisabled={disabled} onChange={onSelectHandler} From 7924ffafbba395b2b42a1fcb406d906969347051 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:47:58 +0100 Subject: [PATCH 09/15] fix: revamp cluster multi picker --- src/components/pickers/ClusterMultiPicker.tsx | 80 ++++++++++--------- src/consts.ts | 25 +++--- 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/src/components/pickers/ClusterMultiPicker.tsx b/src/components/pickers/ClusterMultiPicker.tsx index d80eb7cf7..ca4ac03a7 100644 --- a/src/components/pickers/ClusterMultiPicker.tsx +++ b/src/components/pickers/ClusterMultiPicker.tsx @@ -1,56 +1,62 @@ -import { type ChangeEvent, type DetailedHTMLProps, type InputHTMLAttributes, memo, useCallback, useMemo } from "react"; +import { type DetailedHTMLProps, type InputHTMLAttributes, memo, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import Select, { type MultiValue } from "react-select"; +import { REACT_SELECT_DEFAULT_CLASSNAMES } from "../../consts.js"; +import type { BaseSelectOption } from "../../types.js"; export interface ClusterMultiPickerProps extends Omit, HTMLInputElement>, "onChange"> { label?: string; clusters: Set; value: string[]; + required?: boolean; + disabled?: boolean; onChange(clusters: string[]): void; } -const ClusterMultiPicker = memo((props: ClusterMultiPickerProps) => { - const { clusters, onChange, label, value, disabled } = props; +const ClusterMultiPicker = memo(({ clusters, onChange, label, value, disabled, required }: ClusterMultiPickerProps) => { + const { t } = useTranslation("zigbee"); - const onChangeHandler = useCallback( - (e: ChangeEvent) => { - const { checked: isChecked, name } = e.target; - let newVal = [...value]; + const options = useMemo((): BaseSelectOption[] => { + const options: BaseSelectOption[] = []; - if (isChecked) { - newVal.push(name); - } else { - newVal = newVal.filter((v) => v !== name); - } + for (const cluster of clusters) { + options.push({ label: cluster, value: cluster }); + } - onChange(newVal); - }, - [onChange, value], - ); + options.sort((elA, elB) => elA.label!.localeCompare(elB.label!)); + + return options; + }, [clusters]); - const options = useMemo( - () => - Array.from(clusters) - .sort((a, b) => a.toString().localeCompare(b.toString())) - .map((cluster) => ( - - )), - [clusters, onChangeHandler, value, disabled], + const selected = useMemo>( + () => (value == null ? [] : options.filter((o) => value.includes(o.value))), + [value, options], ); return (
- {label && {label}} -
{options}
+ {label && ( + + {label} + {required ? " *" : ""} + + )} + + {currentDevice.friendly_name} + + ) : ( + t(($) => $.select_device) + ) } - > - - {items} - + aria-label={t(($) => $.select_device)} + options={options} + isSearchable + onChange={onSelectHandler} + className="min-w-48 me-2" + classNames={REACT_SELECT_DEFAULT_CLASSNAMES} + /> ); }); diff --git a/src/components/group-page/HeaderGroupSelector.tsx b/src/components/group-page/HeaderGroupSelector.tsx index eab348d58..a713f0479 100644 --- a/src/components/group-page/HeaderGroupSelector.tsx +++ b/src/components/group-page/HeaderGroupSelector.tsx @@ -1,14 +1,11 @@ -import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { type JSX, memo, useMemo } from "react"; +import { memo, type ReactNode, useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Link } from "react-router"; -import { useSearch } from "../../hooks/useSearch.js"; +import { useNavigate } from "react-router"; +import Select, { type SingleValue } from "react-select"; +import { REACT_SELECT_DEFAULT_CLASSNAMES } from "../../consts.js"; import type { TabName } from "../../pages/GroupPage.js"; import { API_URLS, useAppStore } from "../../store.js"; -import type { Group } from "../../types.js"; -import DialogDropdown from "../DialogDropdown.js"; -import DebouncedInput from "../form-fields/DebouncedInput.js"; +import type { BaseSelectOption, Group } from "../../types.js"; import SourceDot from "../SourceDot.js"; interface HeaderGroupSelectorProps { @@ -17,13 +14,27 @@ interface HeaderGroupSelectorProps { tab?: TabName; } +interface SelectOption extends BaseSelectOption { + name: string; + link: string; +} + const HeaderGroupSelector = memo(({ currentSourceIdx, currentGroup, tab = "devices" }: HeaderGroupSelectorProps) => { - const [searchTerm, normalizedSearchTerm, setSearchTerm] = useSearch(); const { t } = useTranslation("common"); const groups = useAppStore((state) => state.groups); + const navigate = useNavigate(); + + const onSelectHandler = useCallback( + (option: SingleValue) => { + if (option) { + navigate(option.link); + } + }, + [navigate], + ); - const items = useMemo(() => { - const elements: JSX.Element[] = []; + const options = useMemo(() => { + const elements: SelectOption[] = []; for (let sourceIdx = 0; sourceIdx < API_URLS.length; sourceIdx++) { for (const group of groups[sourceIdx]) { @@ -31,45 +42,43 @@ const HeaderGroupSelector = memo(({ currentSourceIdx, currentGroup, tab = "devic continue; } - if (normalizedSearchTerm.length > 0 && !group.friendly_name.toLowerCase().includes(normalizedSearchTerm)) { - continue; - } - - elements.push( -
  • - setSearchTerm("")} className="dropdown-item"> + elements.push({ + value: group.friendly_name, + label: ( + <> {group.friendly_name} - -
  • , - ); + + ), + name: `${sourceIdx} ${group.friendly_name}`, + link: `/group/${sourceIdx}/${group.id}/${tab}`, + }); } } - elements.sort((elA, elB) => elA.key!.localeCompare(elB.key!)); + elements.sort((elA, elB) => elA.name.localeCompare(elB.name)); return elements; - }, [groups, normalizedSearchTerm, currentSourceIdx, currentGroup, tab, setSearchTerm]); + }, [groups, currentSourceIdx, currentGroup, tab]); return ( - - {currentSourceIdx !== undefined && } - {currentGroup ? currentGroup.friendly_name : t(($) => $.unknown_group)} - +