From 3556531d3823946e24d94b659a24dc42e819b2e5 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 27 Jan 2023 12:54:21 +0700 Subject: [PATCH 1/7] front: the user can re-order his dashboards --- front/package-lock.json | 379 +++++++----------- front/package.json | 2 + front/src/config/i18n/en.json | 4 + front/src/config/i18n/fr.json | 4 + front/src/routes/dashboard/DashboardPage.jsx | 5 + front/src/routes/dashboard/EditBoxColumns.jsx | 33 +- .../routes/dashboard/ReorderDashbordList.jsx | 85 ++++ front/src/routes/dashboard/index.js | 39 +- front/src/routes/dashboard/style.css | 12 + 9 files changed, 323 insertions(+), 240 deletions(-) create mode 100644 front/src/routes/dashboard/ReorderDashbordList.jsx diff --git a/front/package-lock.json b/front/package-lock.json index 543a423843..4ebf444089 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -31,6 +31,8 @@ "react-big-calendar": "^0.40.0", "react-clock": "^3.1.0", "react-datepicker": "^3.8.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-select": "^4.3.1", "unistore": "^3.5.2", "useragent-parser-js": "^1.0.3", @@ -3573,11 +3575,14 @@ } }, "node_modules/@babel/runtime": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.2.tgz", - "integrity": "sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==", + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", "dependencies": { - "regenerator-runtime": "^0.13.2" + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/template": { @@ -3861,22 +3866,6 @@ } } }, - "node_modules/@emotion/react/node_modules/@babel/runtime": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emotion/react/node_modules/regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" - }, "node_modules/@emotion/serialize": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz", @@ -4400,6 +4389,21 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "node_modules/@relative-ci/agent": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@relative-ci/agent/-/agent-2.0.0.tgz", @@ -5008,7 +5012,7 @@ "version": "16.3.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.2.tgz", "integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==", - "dev": true + "devOptional": true }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -7457,18 +7461,6 @@ "npm": ">=6" } }, - "node_modules/babel-plugin-macros/node_modules/@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/babel-plugin-macros/node_modules/is-core-module": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", @@ -7487,12 +7479,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "node_modules/babel-plugin-macros/node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, "node_modules/babel-plugin-macros/node_modules/resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -11952,6 +11938,16 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "node_modules/dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -26051,17 +26047,6 @@ "react-dom": "^16.14.0 || ^17" } }, - "node_modules/react-big-calendar/node_modules/@babel/runtime": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", - "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/react-big-calendar/node_modules/csstype": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", @@ -26096,11 +26081,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/react-big-calendar/node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" - }, "node_modules/react-clock": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/react-clock/-/react-clock-3.1.0.tgz", @@ -26135,6 +26115,43 @@ "react-dom": "^16.9.0 || ^17" } }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -26202,17 +26219,6 @@ "react-dom": ">=16.3.0" } }, - "node_modules/react-overlays/node_modules/@babel/runtime": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", - "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/react-overlays/node_modules/csstype": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", @@ -26227,11 +26233,6 @@ "csstype": "^3.0.2" } }, - "node_modules/react-overlays/node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" - }, "node_modules/react-popper": { "version": "1.3.11", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.11.tgz", @@ -26276,27 +26277,11 @@ "react-dom": "^16.8.0 || ^17.0.0" } }, - "node_modules/react-select/node_modules/@babel/runtime": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/react-select/node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, - "node_modules/react-select/node_modules/regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" - }, "node_modules/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -26390,6 +26375,14 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", + "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -26409,9 +26402,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regenerator-transform": { "version": "0.15.0", @@ -26422,24 +26415,6 @@ "@babel/runtime": "^7.8.4" } }, - "node_modules/regenerator-transform/node_modules/@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/regenerator-transform/node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, "node_modules/regex-cache": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", @@ -33211,18 +33186,6 @@ "node": ">=10.0.0" } }, - "node_modules/workbox-build/node_modules/@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/workbox-build/node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -33247,12 +33210,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/workbox-build/node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, "node_modules/workbox-build/node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -36237,11 +36194,11 @@ } }, "@babel/runtime": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.2.tgz", - "integrity": "sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==", + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", "requires": { - "regenerator-runtime": "^0.13.2" + "regenerator-runtime": "^0.13.11" } }, "@babel/template": { @@ -36462,21 +36419,6 @@ "@emotion/utils": "^1.0.0", "@emotion/weak-memoize": "^0.2.5", "hoist-non-react-statics": "^3.3.1" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" - } } }, "@emotion/serialize": { @@ -36915,6 +36857,21 @@ "@prefresh/utils": "^1.1.2" } }, + "@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "@relative-ci/agent": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@relative-ci/agent/-/agent-2.0.0.tgz", @@ -37372,7 +37329,7 @@ "version": "16.3.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.2.tgz", "integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==", - "dev": true + "devOptional": true }, "@types/parse-json": { "version": "4.0.0", @@ -39323,15 +39280,6 @@ "resolve": "^1.19.0" }, "dependencies": { - "@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, "is-core-module": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", @@ -39347,12 +39295,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, "resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -42907,6 +42849,16 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "requires": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -53819,14 +53771,6 @@ "uncontrollable": "^7.2.1" }, "dependencies": { - "@babel/runtime": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", - "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, "csstype": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", @@ -53860,11 +53804,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" } } }, @@ -53891,6 +53830,26 @@ "react-popper": "^1.3.8" } }, + "react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "requires": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + } + }, + "react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "requires": { + "dnd-core": "^16.0.1" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -53941,14 +53900,6 @@ "warning": "^4.0.3" }, "dependencies": { - "@babel/runtime": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", - "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, "csstype": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", @@ -53962,11 +53913,6 @@ "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } - }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" } } }, @@ -54004,23 +53950,10 @@ "react-transition-group": "^4.3.0" }, "dependencies": { - "@babel/runtime": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, "memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" } } }, @@ -54097,6 +54030,14 @@ "picomatch": "^2.2.1" } }, + "redux": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", + "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -54113,9 +54054,9 @@ } }, "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "regenerator-transform": { "version": "0.15.0", @@ -54124,23 +54065,6 @@ "dev": true, "requires": { "@babel/runtime": "^7.8.4" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - } } }, "regex-cache": { @@ -59519,15 +59443,6 @@ "workbox-window": "6.5.3" }, "dependencies": { - "@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -59546,12 +59461,6 @@ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, "source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", diff --git a/front/package.json b/front/package.json index 11f91ed621..48b2a5cd34 100644 --- a/front/package.json +++ b/front/package.json @@ -69,6 +69,8 @@ "react-big-calendar": "^0.40.0", "react-clock": "^3.1.0", "react-datepicker": "^3.8.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-select": "^4.3.1", "unistore": "^3.5.2", "useragent-parser-js": "^1.0.3", diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index a17bcb472e..4406ac81ca 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -196,6 +196,10 @@ "noDashboardSentenceBottom": "Click on the \"New\" button to create a new dashboard", "gatewayInstanceNotFoundError": "Your Gladys instance is not connected to the Gladys Gateway", "editDashboardNameLabel": "Name", + "reorderDashboardsButton": "Re-order my dashboards", + "reorderDashboardsButtonActive": "Save the order", + "reorderDashboardsButtonSaving": "Saving...", + "reorderDashboardsLabel": "Re-order this list, then click on \"Save the order\"", "boxTitle": { "weather": "Weather", "temperature-in-room": "Temperature in room", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index ea718fe60b..43510b0483 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -196,6 +196,10 @@ "noDashboardSentenceBottom": "Cliquez sur le bouton \"Nouveau\" pour créer un tableau de bord.", "gatewayInstanceNotFoundError": "Votre instance Gladys n'est pas connectée à Gladys Plus.", "editDashboardNameLabel": "Nom", + "reorderDashboardsButton": "Re-ordonner mes tableaux de bords", + "reorderDashboardsButtonActive": "J'ai fini !", + "reorderDashboardsButtonSaving": "Enregistrement...", + "reorderDashboardsLabel": "Re-ordonnez cette liste, puis cliquez sur \"J'ai fini\"", "boxTitle": { "weather": "Météo", "temperature-in-room": "Température de la pièce", diff --git a/front/src/routes/dashboard/DashboardPage.jsx b/front/src/routes/dashboard/DashboardPage.jsx index 4e5aab4640..aa1bb21aa7 100644 --- a/front/src/routes/dashboard/DashboardPage.jsx +++ b/front/src/routes/dashboard/DashboardPage.jsx @@ -92,6 +92,7 @@ const DashboardPage = ({ children, ...props }) => ( )} {props.dashboardEditMode && ( ( updateNewSelectedBox={props.updateNewSelectedBox} removeBox={props.removeBox} updateBoxConfig={props.updateBoxConfig} + showReorderDashboard={props.showReorderDashboard} + toggleReorderDashboard={props.toggleReorderDashboard} + updateDashboardList={props.updateDashboardList} + savingNewDashboardList={props.savingNewDashboardList} /> )} {props.dashboardEditMode && } diff --git a/front/src/routes/dashboard/EditBoxColumns.jsx b/front/src/routes/dashboard/EditBoxColumns.jsx index c376ae7b12..167a493b6b 100644 --- a/front/src/routes/dashboard/EditBoxColumns.jsx +++ b/front/src/routes/dashboard/EditBoxColumns.jsx @@ -2,6 +2,7 @@ import { Text, Localizer } from 'preact-i18n'; import cx from 'classnames'; import EditBox from './EditBox'; import EditAddBoxButton from './EditAddBoxButton'; +import ReorderDashbordList from './ReorderDashbordList'; import style from './style.css'; const EditBoxColumns = ({ children, ...props }) => ( @@ -24,8 +25,8 @@ const EditBoxColumns = ({ children, ...props }) => ( )} -
-
+
+
+
+
+ +
+
+
+
+
+ +
{props.homeDashboard && diff --git a/front/src/routes/dashboard/ReorderDashbordList.jsx b/front/src/routes/dashboard/ReorderDashbordList.jsx new file mode 100644 index 0000000000..4774d517df --- /dev/null +++ b/front/src/routes/dashboard/ReorderDashbordList.jsx @@ -0,0 +1,85 @@ +import { useRef } from 'preact/hooks'; +import { Component } from 'preact'; +import { Text } from 'preact-i18n'; +import update from 'immutability-helper'; +import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; + +const DASHBOARD_LIST_ITEM_TYPE = 'DASHBOARD_LIST_ITEM'; + +const DashboardListItem = ({ children, ...props }) => { + const { index } = props; + const ref = useRef(null); + const [{ isDragging }, drag] = useDrag(() => ({ + type: DASHBOARD_LIST_ITEM_TYPE, + item: () => { + return { index }; + }, + collect: monitor => ({ + isDragging: !!monitor.isDragging() + }) + })); + const [{ isActive }, drop] = useDrop({ + accept: DASHBOARD_LIST_ITEM_TYPE, + collect: monitor => ({ + isActive: monitor.canDrop() && monitor.isOver() + }), + drop(item) { + if (!ref.current) { + return; + } + props.insertAtPosition(item.index, index); + } + }); + drag(drop(ref)); + + return ( +
  • + {props.name} +
  • + ); +}; + +class RedorderDashboardList extends Component { + insertAtPosition = (sourceIndex, destinationIndex) => { + const { dashboards } = this.props; + const element = dashboards[sourceIndex]; + const newDashboards = update(dashboards, { + $splice: [ + [sourceIndex, 1], + [destinationIndex, 0, element] + ] + }); + this.props.updateDashboardList(newDashboards); + }; + + render({ dashboards }, {}) { + return ( + + +
      + {dashboards && + dashboards.map((dashboard, index) => ( + + ))} +
    +
    + ); + } +} + +export default RedorderDashboardList; diff --git a/front/src/routes/dashboard/index.js b/front/src/routes/dashboard/index.js index 86ac5e7bd3..eed07da38d 100644 --- a/front/src/routes/dashboard/index.js +++ b/front/src/routes/dashboard/index.js @@ -11,7 +11,6 @@ extend('$auto', (value, object) => { return object ? update(object, value) : update({}, value); }); -@connect('user,fullScreen,currentUrl,httpClient,gatewayAccountExpired', actions) class Dashboard extends Component { toggleDashboardDropdown = () => { this.setState(prevState => { @@ -78,6 +77,7 @@ class Dashboard extends Component { await this.getDashboards(); if (this.state.currentDashboardSelector) { await this.getCurrentDashboard(); + this.editDashboard(); } }; @@ -319,12 +319,39 @@ class Dashboard extends Component { this.props.setFullScreen(isFullScreen); }; + toggleReorderDashboard = () => { + if (this.state.showReorderDashboard) { + this.saveNewDashboardList(); + } else { + this.setState({ showReorderDashboard: !this.state.showReorderDashboard }); + } + }; + + updateDashboardList = newDashboards => { + this.setState({ + dashboards: newDashboards + }); + }; + + saveNewDashboardList = async () => { + await this.setState({ + savingNewDashboardList: true + }); + setTimeout(() => { + this.setState({ + savingNewDashboardList: false, + showReorderDashboard: false + }); + }, 500); + }; + constructor(props) { super(props); this.props = props; this.state = { dashboardDropdownOpened: false, dashboardEditMode: false, + showReorderDashboard: false, browserFullScreenCompatible: this.isBrowserFullScreenCompatible(), dashboards: [], newSelectedBoxType: {}, @@ -364,7 +391,9 @@ class Dashboard extends Component { dashboardValidationError, dashboardAlreadyExistError, unknownError, - askDeleteDashboard + askDeleteDashboard, + showReorderDashboard, + savingNewDashboardList } ) { const dashboardConfigured = @@ -411,9 +440,13 @@ class Dashboard extends Component { deleteCurrentDashboard={this.deleteCurrentDashboard} askDeleteDashboard={askDeleteDashboard} fullScreen={props.fullScreen} + showReorderDashboard={showReorderDashboard} + toggleReorderDashboard={this.toggleReorderDashboard} + updateDashboardList={this.updateDashboardList} + savingNewDashboardList={savingNewDashboardList} /> ); } } -export default Dashboard; +export default connect('user,fullScreen,currentUrl,httpClient,gatewayAccountExpired', actions)(Dashboard); diff --git a/front/src/routes/dashboard/style.css b/front/src/routes/dashboard/style.css index 61fe47589b..606fe648da 100644 --- a/front/src/routes/dashboard/style.css +++ b/front/src/routes/dashboard/style.css @@ -45,3 +45,15 @@ border-radius: 3px; } } + +.collapse { + max-height: 0; + transition: max-height 0.25s ease-out; + overflow: hidden; +} + +.showCollapse { + max-height: 500px; + overflow: auto; + transition: max-height 0.5s ease-in; +} From 20d83e78fe0f3111741dd0997a428ee53064f270 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Mon, 30 Jan 2023 12:23:46 +0700 Subject: [PATCH 2/7] Add position attribute in dashboard + add new route to re-order dashboard --- front/src/routes/dashboard/index.js | 13 +++++--- .../api/controllers/dashboard.controller.js | 14 ++++++++- server/api/routes.js | 4 +++ server/lib/dashboard/dashboard.get.js | 1 + .../lib/dashboard/dashboard.getBySelector.js | 1 + server/lib/dashboard/dashboard.updateOrder.js | 28 +++++++++++++++++ server/lib/dashboard/index.js | 2 ++ .../20230130044921-add-position-dashboard.js | 10 +++++++ server/models/dashboard.js | 4 +++ .../dashboard/dashboard.controller.test.js | 12 +++++++- server/test/lib/dashboard/dashboard.test.js | 30 +++++++++++++++++++ 11 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 server/lib/dashboard/dashboard.updateOrder.js create mode 100644 server/migrations/20230130044921-add-position-dashboard.js diff --git a/front/src/routes/dashboard/index.js b/front/src/routes/dashboard/index.js index eed07da38d..8b3f40bf7c 100644 --- a/front/src/routes/dashboard/index.js +++ b/front/src/routes/dashboard/index.js @@ -77,7 +77,6 @@ class Dashboard extends Component { await this.getDashboards(); if (this.state.currentDashboardSelector) { await this.getCurrentDashboard(); - this.editDashboard(); } }; @@ -337,12 +336,18 @@ class Dashboard extends Component { await this.setState({ savingNewDashboardList: true }); - setTimeout(() => { + try { + const dashboardSelectors = this.state.dashboards.map(d => d.selector); + await this.props.httpClient.post('/api/v1/dashboard/order', dashboardSelectors); this.setState({ - savingNewDashboardList: false, showReorderDashboard: false }); - }, 500); + } catch (e) { + console.error(e); + } + this.setState({ + savingNewDashboardList: false + }); }; constructor(props) { diff --git a/server/api/controllers/dashboard.controller.js b/server/api/controllers/dashboard.controller.js index 14fc3d66dc..fcf2b343cf 100644 --- a/server/api/controllers/dashboard.controller.js +++ b/server/api/controllers/dashboard.controller.js @@ -16,7 +16,7 @@ const asyncMiddleware = require('../middlewares/asyncMiddleware'); * @apiSuccess {Array} [boxes] Array of boxes in the dashboard. */ -module.exports = function HouseController(gladys) { +module.exports = function DashboardController(gladys) { /** * @api {post} /api/v1/dashboard create * @apiName createDashoard @@ -52,6 +52,17 @@ module.exports = function HouseController(gladys) { res.json(dashboard); } + /** + * @api {post} /api/v1/dashboard/order updateOrder + * @apiName updateOrder + * @apiGroup Dashboard + * @apiParam {Array} [selectors] Array of selectors in new order. + */ + async function updateOrder(req, res) { + await gladys.dashboard.updateOrder(req.user.id, req.body); + res.json({ success: true }); + } + /** * @api {get} /api/v1/dashboard/:dashboard_selector getBySelector * @apiName getBySelector @@ -82,5 +93,6 @@ module.exports = function HouseController(gladys) { get: asyncMiddleware(get), getBySelector: asyncMiddleware(getBySelector), update: asyncMiddleware(update), + updateOrder: asyncMiddleware(updateOrder), }); }; diff --git a/server/api/routes.js b/server/api/routes.js index 6e2f162e4b..cb8edcc46a 100644 --- a/server/api/routes.js +++ b/server/api/routes.js @@ -174,6 +174,10 @@ function getRoutes(gladys) { authenticated: true, controller: dashboardController.create, }, + 'post /api/v1/dashboard/order': { + authenticated: true, + controller: dashboardController.updateOrder, + }, 'get /api/v1/dashboard/:dashboard_selector': { authenticated: true, controller: dashboardController.getBySelector, diff --git a/server/lib/dashboard/dashboard.get.js b/server/lib/dashboard/dashboard.get.js index 00b84509d1..cdb5ed50df 100644 --- a/server/lib/dashboard/dashboard.get.js +++ b/server/lib/dashboard/dashboard.get.js @@ -15,6 +15,7 @@ async function get(userId) { where: { user_id: userId, }, + order: [['position', 'ASC']], raw: true, }); return dashboards; diff --git a/server/lib/dashboard/dashboard.getBySelector.js b/server/lib/dashboard/dashboard.getBySelector.js index 1cdfae66ca..1f0ee7648c 100644 --- a/server/lib/dashboard/dashboard.getBySelector.js +++ b/server/lib/dashboard/dashboard.getBySelector.js @@ -11,6 +11,7 @@ const { NotFoundError } = require('../../utils/coreErrors'); */ async function getBySelector(userId, selector) { const dashboard = await db.Dashboard.findOne({ + attributes: ['id', 'name', 'selector', 'type', 'created_at', 'updated_at', 'boxes'], where: { user_id: userId, selector, diff --git a/server/lib/dashboard/dashboard.updateOrder.js b/server/lib/dashboard/dashboard.updateOrder.js new file mode 100644 index 0000000000..e595d4f146 --- /dev/null +++ b/server/lib/dashboard/dashboard.updateOrder.js @@ -0,0 +1,28 @@ +const db = require('../../models'); +const Promise = require('bluebird'); + +/** + * @description Update a dashboard. + * @param {string} userId - The userId querying. + * @param {Array} dashboards - Dashboard selectors new order. + * @example + * gladys.dashboard.updateOrder('483b68cb-15ef-4ea3-80df-1e1bed5b402d', ['my-dashboard', 'other-dashboard']); + */ +async function updateOrder(userId, dashboards) { + // Foreach dashboard, update its position + await Promise.each(dashboards, async (dashboard, index) => { + await db.Dashboard.update( + { position: index }, + { + where: { + user_id: userId, + selector: dashboard, + }, + }, + ); + }); +} + +module.exports = { + updateOrder, +}; diff --git a/server/lib/dashboard/index.js b/server/lib/dashboard/index.js index 144c0a0390..37ac3ca9b8 100644 --- a/server/lib/dashboard/index.js +++ b/server/lib/dashboard/index.js @@ -3,6 +3,7 @@ const { get } = require('./dashboard.get'); const { destroy } = require('./dashboard.destroy'); const { getBySelector } = require('./dashboard.getBySelector'); const { update } = require('./dashboard.update'); +const { updateOrder } = require('./dashboard.updateOrder'); const Dashboard = function Dashboard() {}; @@ -11,5 +12,6 @@ Dashboard.prototype.destroy = destroy; Dashboard.prototype.get = get; Dashboard.prototype.getBySelector = getBySelector; Dashboard.prototype.update = update; +Dashboard.prototype.updateOrder = updateOrder; module.exports = Dashboard; diff --git a/server/migrations/20230130044921-add-position-dashboard.js b/server/migrations/20230130044921-add-position-dashboard.js new file mode 100644 index 0000000000..c5a866e53f --- /dev/null +++ b/server/migrations/20230130044921-add-position-dashboard.js @@ -0,0 +1,10 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('t_dashboard', 'position', { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + }); + }, + down: async (queryInterface, Sequelize) => {}, +}; diff --git a/server/models/dashboard.js b/server/models/dashboard.js index a32a4d648a..eee2d5a018 100644 --- a/server/models/dashboard.js +++ b/server/models/dashboard.js @@ -55,6 +55,10 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, type: DataTypes.ENUM(DASHBOARD_TYPE_LIST), }, + position: { + allowNull: false, + type: DataTypes.INTEGER, + }, selector: { allowNull: false, unique: true, diff --git a/server/test/controllers/dashboard/dashboard.controller.test.js b/server/test/controllers/dashboard/dashboard.controller.test.js index 36fc7696fb..54936bbea2 100644 --- a/server/test/controllers/dashboard/dashboard.controller.test.js +++ b/server/test/controllers/dashboard/dashboard.controller.test.js @@ -8,6 +8,7 @@ describe('POST /api/v1/dashboard', () => { .send({ name: 'my dashboard', type: 'main', + position: 0, boxes: [ [ { @@ -56,7 +57,6 @@ describe('GET /api/v1/dashboard/:dashboard_selector', () => { id: '854dda11-80c0-4476-843b-65cbc95c6a85', name: 'Test dashboard', selector: 'test-dashboard', - user_id: '0cd30aef-9c4e-4a23-88e3-3547971296e5', type: 'main', boxes: [ [ @@ -102,6 +102,16 @@ describe('PATCH /api/v1/dashboard/:dashboard_selector', () => { }); }); +describe('POST /api/v1/dashboard/order', () => { + it('should update order of dashboards', async () => { + await authenticatedRequest + .post('/api/v1/dashboard/order') + .send(['test-dashboard']) + .expect('Content-Type', /json/) + .expect(200); + }); +}); + describe('DELETE /api/v1/dashboard/:dashboard_selector', () => { it('should patch dashboard', async () => { await authenticatedRequest diff --git a/server/test/lib/dashboard/dashboard.test.js b/server/test/lib/dashboard/dashboard.test.js index c4b0624b53..b7ef928167 100644 --- a/server/test/lib/dashboard/dashboard.test.js +++ b/server/test/lib/dashboard/dashboard.test.js @@ -2,6 +2,7 @@ const { expect, assert } = require('chai'); const { DASHBOARD_BOX_TYPE, DASHBOARD_TYPE } = require('../../../utils/constants'); const Dashboard = require('../../../lib/dashboard'); +const db = require('../../../models'); describe('dashboard.create', () => { const dashboard = new Dashboard(); @@ -9,6 +10,7 @@ describe('dashboard.create', () => { const newDashboard = await dashboard.create('0cd30aef-9c4e-4a23-88e3-3547971296e5', { name: 'My new dashboard', type: DASHBOARD_TYPE.MAIN, + position: 0, boxes: [ [ { @@ -76,6 +78,34 @@ describe('dashboard.update', () => { }); }); +describe('dashboard.updateOrder', () => { + const dashboard = new Dashboard(); + it('should update the order of dashboards', async () => { + const newDashboard = await dashboard.create('0cd30aef-9c4e-4a23-88e3-3547971296e5', { + name: 'My new dashboard', + type: DASHBOARD_TYPE.MAIN, + position: 0, + boxes: [ + [ + { + type: DASHBOARD_BOX_TYPE.USER_PRESENCE, + }, + ], + ], + }); + await dashboard.updateOrder('0cd30aef-9c4e-4a23-88e3-3547971296e5', [newDashboard.selector, 'test-dashboard']); + const dashboardsInNewOrder = await db.Dashboard.findAll({ + attributes: ['selector', 'position'], + order: [['position', 'asc']], + raw: true, + }); + expect(dashboardsInNewOrder).to.deep.equal([ + { selector: 'my-new-dashboard', position: 0 }, + { selector: 'test-dashboard', position: 1 }, + ]); + }); +}); + describe('dashboard.destroy', () => { const dashboard = new Dashboard(); it('should destroy a dashoard', async () => { From e2d9caa95ac1f5a7d0e6e6020b952476054007f8 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Mon, 30 Jan 2023 12:28:09 +0700 Subject: [PATCH 3/7] Fix eslint --- server/lib/dashboard/dashboard.updateOrder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/lib/dashboard/dashboard.updateOrder.js b/server/lib/dashboard/dashboard.updateOrder.js index e595d4f146..f3861c0146 100644 --- a/server/lib/dashboard/dashboard.updateOrder.js +++ b/server/lib/dashboard/dashboard.updateOrder.js @@ -1,10 +1,10 @@ -const db = require('../../models'); const Promise = require('bluebird'); +const db = require('../../models'); /** * @description Update a dashboard. * @param {string} userId - The userId querying. - * @param {Array} dashboards - Dashboard selectors new order. + * @param {Array} dashboards - Dashboard selectors new order. * @example * gladys.dashboard.updateOrder('483b68cb-15ef-4ea3-80df-1e1bed5b402d', ['my-dashboard', 'other-dashboard']); */ From fa607d8320831bfb0d1409f53a42a0d3cc064fe3 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Mon, 30 Jan 2023 17:26:50 +0700 Subject: [PATCH 4/7] New dashboard edition UI --- front/src/components/app.jsx | 4 + front/src/config/i18n/en.json | 5 +- front/src/config/i18n/fr.json | 5 +- front/src/routes/dashboard/DashboardPage.jsx | 133 +++---- .../{ => edit-dashboard}/EditActions.jsx | 0 .../{ => edit-dashboard}/EditAddBoxButton.jsx | 2 +- .../{ => edit-dashboard}/EditBox.jsx | 18 +- .../{ => edit-dashboard}/EditBoxColumns.jsx | 36 +- .../edit-dashboard/EditDashboard.jsx | 79 ++++ .../ReorderDashbordList.jsx | 27 +- .../routes/dashboard/edit-dashboard/index.js | 345 ++++++++++++++++++ front/src/routes/dashboard/index.js | 2 +- .../routes/dashboard/new-dashboard/index.js | 20 +- front/src/routes/dashboard/style.css | 14 + server/lib/dashboard/dashboard.create.js | 13 + server/models/dashboard.js | 1 + .../dashboard/dashboard.controller.test.js | 1 + 17 files changed, 550 insertions(+), 155 deletions(-) rename front/src/routes/dashboard/{ => edit-dashboard}/EditActions.jsx (100%) rename front/src/routes/dashboard/{ => edit-dashboard}/EditAddBoxButton.jsx (92%) rename front/src/routes/dashboard/{ => edit-dashboard}/EditBox.jsx (53%) rename front/src/routes/dashboard/{ => edit-dashboard}/EditBoxColumns.jsx (64%) create mode 100644 front/src/routes/dashboard/edit-dashboard/EditDashboard.jsx rename front/src/routes/dashboard/{ => edit-dashboard}/ReorderDashbordList.jsx (73%) create mode 100644 front/src/routes/dashboard/edit-dashboard/index.js diff --git a/front/src/components/app.jsx b/front/src/components/app.jsx index 82a6de31ac..69f7f49788 100644 --- a/front/src/components/app.jsx +++ b/front/src/components/app.jsx @@ -36,8 +36,11 @@ import SignupPreferences from '../routes/signup/3-preferences'; import SignupConfigureHouse from '../routes/signup/4-configure-house'; import SignupSuccess from '../routes/signup/5-success'; +// Dashboard import Dashboard from '../routes/dashboard'; import NewDashboard from '../routes/dashboard/new-dashboard'; +import EditDashboard from '../routes/dashboard/edit-dashboard'; + import Device from '../routes/device'; import IntegrationPage from '../routes/integration'; import ChatPage from '../routes/chat'; @@ -184,6 +187,7 @@ const AppRouter = connect( + diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 4406ac81ca..7e27bd5099 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -196,10 +196,7 @@ "noDashboardSentenceBottom": "Click on the \"New\" button to create a new dashboard", "gatewayInstanceNotFoundError": "Your Gladys instance is not connected to the Gladys Gateway", "editDashboardNameLabel": "Name", - "reorderDashboardsButton": "Re-order my dashboards", - "reorderDashboardsButtonActive": "Save the order", - "reorderDashboardsButtonSaving": "Saving...", - "reorderDashboardsLabel": "Re-order this list, then click on \"Save the order\"", + "editDashboardMyDashboards": "My dashboards", "boxTitle": { "weather": "Weather", "temperature-in-room": "Temperature in room", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 43510b0483..c87a01cd4e 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -196,10 +196,7 @@ "noDashboardSentenceBottom": "Cliquez sur le bouton \"Nouveau\" pour créer un tableau de bord.", "gatewayInstanceNotFoundError": "Votre instance Gladys n'est pas connectée à Gladys Plus.", "editDashboardNameLabel": "Nom", - "reorderDashboardsButton": "Re-ordonner mes tableaux de bords", - "reorderDashboardsButtonActive": "J'ai fini !", - "reorderDashboardsButtonSaving": "Enregistrement...", - "reorderDashboardsLabel": "Re-ordonnez cette liste, puis cliquez sur \"J'ai fini\"", + "editDashboardMyDashboards": "Mes tableaux de bords", "boxTitle": { "weather": "Météo", "temperature-in-room": "Température de la pièce", diff --git a/front/src/routes/dashboard/DashboardPage.jsx b/front/src/routes/dashboard/DashboardPage.jsx index aa1bb21aa7..52fde98548 100644 --- a/front/src/routes/dashboard/DashboardPage.jsx +++ b/front/src/routes/dashboard/DashboardPage.jsx @@ -2,16 +2,10 @@ import { Text } from 'preact-i18n'; import { Link } from 'preact-router/match'; import cx from 'classnames'; import BoxColumns from './BoxColumns'; -import EditBoxColumns from './EditBoxColumns'; import EmptyState from './EmptyState'; -import EditActions from './EditActions'; import style from './style.css'; -const marginBottom = { - marginBottom: '10rem' -}; - const DashboardPage = ({ children, ...props }) => (
    @@ -19,97 +13,62 @@ const DashboardPage = ({ children, ...props }) => (
    -
    - {!props.dashboardEditMode && ( - + )} +
    + +
    + {!props.dashboardNotConfigured && props.browserFullScreenCompatible && false && ( +
    + + )} + {props.currentDashboard && ( + + )}
    - )} +
    + {props.gatewayInstanceNotFound && (
    )} - {props.dashboardNotConfigured && !props.dashboardEditMode && ( - - )} - {!props.dashboardNotConfigured && !props.dashboardEditMode && ( - - )} - {props.dashboardEditMode && ( - - )} - {props.dashboardEditMode && } + {props.dashboardNotConfigured && } + {!props.dashboardNotConfigured && }
    diff --git a/front/src/routes/dashboard/EditActions.jsx b/front/src/routes/dashboard/edit-dashboard/EditActions.jsx similarity index 100% rename from front/src/routes/dashboard/EditActions.jsx rename to front/src/routes/dashboard/edit-dashboard/EditActions.jsx diff --git a/front/src/routes/dashboard/EditAddBoxButton.jsx b/front/src/routes/dashboard/edit-dashboard/EditAddBoxButton.jsx similarity index 92% rename from front/src/routes/dashboard/EditAddBoxButton.jsx rename to front/src/routes/dashboard/edit-dashboard/EditAddBoxButton.jsx index f1e37f6fd9..c080744b23 100644 --- a/front/src/routes/dashboard/EditAddBoxButton.jsx +++ b/front/src/routes/dashboard/edit-dashboard/EditAddBoxButton.jsx @@ -1,5 +1,5 @@ import { Text } from 'preact-i18n'; -import { DASHBOARD_BOX_TYPE_LIST } from '../../../../server/utils/constants'; +import { DASHBOARD_BOX_TYPE_LIST } from '../../../../../server/utils/constants'; const addBox = (addBoxFunction, x) => () => { addBoxFunction(x); diff --git a/front/src/routes/dashboard/EditBox.jsx b/front/src/routes/dashboard/edit-dashboard/EditBox.jsx similarity index 53% rename from front/src/routes/dashboard/EditBox.jsx rename to front/src/routes/dashboard/edit-dashboard/EditBox.jsx index a6349c84fa..3d281346eb 100644 --- a/front/src/routes/dashboard/EditBox.jsx +++ b/front/src/routes/dashboard/edit-dashboard/EditBox.jsx @@ -1,12 +1,12 @@ -import EditWeatherBox from '../../components/boxs/weather/EditWeatherBox'; -import EditRoomTemperatureBox from '../../components/boxs/room-temperature/EditRoomTemperatureBox'; -import EditRoomHumidityBox from '../../components/boxs/room-humidity/EditRoomHumidityBox'; -import EditCameraBox from '../../components/boxs/camera/EditCamera'; -import EditAtHomeBox from '../../components/boxs/user-presence/EditUserPresenceBox'; -import EditDevicesInRoom from '../../components/boxs/device-in-room/EditDeviceInRoom'; -import EditChart from '../../components/boxs/chart/EditChart'; -import EditEcowatt from '../../components/boxs/ecowatt/EditEcowatt'; -import EditClock from '../../components/boxs/clock/EditClock'; +import EditWeatherBox from '../../../components/boxs/weather/EditWeatherBox'; +import EditRoomTemperatureBox from '../../../components/boxs/room-temperature/EditRoomTemperatureBox'; +import EditRoomHumidityBox from '../../../components/boxs/room-humidity/EditRoomHumidityBox'; +import EditCameraBox from '../../../components/boxs/camera/EditCamera'; +import EditAtHomeBox from '../../../components/boxs/user-presence/EditUserPresenceBox'; +import EditDevicesInRoom from '../../../components/boxs/device-in-room/EditDeviceInRoom'; +import EditChart from '../../../components/boxs/chart/EditChart'; +import EditEcowatt from '../../../components/boxs/ecowatt/EditEcowatt'; +import EditClock from '../../../components/boxs/clock/EditClock'; const Box = ({ children, ...props }) => { switch (props.box.type) { diff --git a/front/src/routes/dashboard/EditBoxColumns.jsx b/front/src/routes/dashboard/edit-dashboard/EditBoxColumns.jsx similarity index 64% rename from front/src/routes/dashboard/EditBoxColumns.jsx rename to front/src/routes/dashboard/edit-dashboard/EditBoxColumns.jsx index 167a493b6b..8cc62d5fd8 100644 --- a/front/src/routes/dashboard/EditBoxColumns.jsx +++ b/front/src/routes/dashboard/edit-dashboard/EditBoxColumns.jsx @@ -2,14 +2,13 @@ import { Text, Localizer } from 'preact-i18n'; import cx from 'classnames'; import EditBox from './EditBox'; import EditAddBoxButton from './EditAddBoxButton'; -import ReorderDashbordList from './ReorderDashbordList'; -import style from './style.css'; +import style from '../style.css'; const EditBoxColumns = ({ children, ...props }) => (
    -

    +

    -

    + {props.dashboardAlreadyExistError && (
    @@ -42,37 +41,10 @@ const EditBoxColumns = ({ children, ...props }) => (
    -
    -
    - -
    -
    -
    -
    -
    - -
    {props.homeDashboard && + props.homeDashboard.boxes && props.homeDashboard.boxes.map((column, x) => (
    ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + +

    +
    + + + + +
    +
    + {props.currentDashboard && ( + + )} +
    +
    +
    +
    +
    + {props.currentDashboard && ( + + )} +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +); + +export default EditDashboard; diff --git a/front/src/routes/dashboard/ReorderDashbordList.jsx b/front/src/routes/dashboard/edit-dashboard/ReorderDashbordList.jsx similarity index 73% rename from front/src/routes/dashboard/ReorderDashbordList.jsx rename to front/src/routes/dashboard/edit-dashboard/ReorderDashbordList.jsx index 4774d517df..77bbcfa7f3 100644 --- a/front/src/routes/dashboard/ReorderDashbordList.jsx +++ b/front/src/routes/dashboard/edit-dashboard/ReorderDashbordList.jsx @@ -1,7 +1,8 @@ import { useRef } from 'preact/hooks'; import { Component } from 'preact'; -import { Text } from 'preact-i18n'; +import cx from 'classnames'; import update from 'immutability-helper'; +import { route } from 'preact-router'; import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; @@ -10,7 +11,7 @@ const DASHBOARD_LIST_ITEM_TYPE = 'DASHBOARD_LIST_ITEM'; const DashboardListItem = ({ children, ...props }) => { const { index } = props; const ref = useRef(null); - const [{ isDragging }, drag] = useDrag(() => ({ + const [{ isDragging }, drag, preview] = useDrag(() => ({ type: DASHBOARD_LIST_ITEM_TYPE, item: () => { return { index }; @@ -31,18 +32,25 @@ const DashboardListItem = ({ children, ...props }) => { props.insertAtPosition(item.index, index); } }); - drag(drop(ref)); + const openEditPage = () => { + route(`/dashboard/${props.selector}/edit`); + }; + preview(drop(ref)); return (
  • - {props.name} + {props.name}
  • ); }; @@ -60,12 +68,9 @@ class RedorderDashboardList extends Component { this.props.updateDashboardList(newDashboards); }; - render({ dashboards }, {}) { + render({ dashboards, currentDashboard }, {}) { return ( -
      {dashboards && dashboards.map((dashboard, index) => ( @@ -73,6 +78,8 @@ class RedorderDashboardList extends Component { index={index} id={dashboard.id} name={dashboard.name} + selector={dashboard.selector} + isSelected={dashboard.id === currentDashboard.id} insertAtPosition={this.insertAtPosition} /> ))} diff --git a/front/src/routes/dashboard/edit-dashboard/index.js b/front/src/routes/dashboard/edit-dashboard/index.js new file mode 100644 index 0000000000..5f756c240e --- /dev/null +++ b/front/src/routes/dashboard/edit-dashboard/index.js @@ -0,0 +1,345 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import { route } from 'preact-router'; +import update from 'immutability-helper'; +import EditDashboardPage from './EditDashboard'; +import get from 'get-value'; + +class EditDashboard extends Component { + getDashboards = async () => { + try { + await this.setState({ + getDashboardsError: false, + loading: true + }); + const dashboards = await this.props.httpClient.get('/api/v1/dashboard'); + let currentDashboardSelector; + if (this.props.dashboardSelector) { + currentDashboardSelector = this.props.dashboardSelector; + } else if (dashboards.length > 0) { + currentDashboardSelector = dashboards[0].selector; + } + await this.setState({ + dashboards, + currentDashboardSelector, + getDashboardsError: false, + loading: false + }); + } catch (e) { + console.error(e); + this.setState({ loading: false }); + const status = get(e, 'response.status'); + const errorMessage = get(e, 'response.error_message'); + // in case we are on the gateway (Gladys Plus) + if (status === 404 && errorMessage === 'NO_INSTANCE_FOUND') { + this.setState({ + gatewayInstanceNotFound: true + }); + } else { + this.setState({ + getDashboardsError: true + }); + } + } + }; + + getCurrentDashboard = async () => { + try { + await this.setState({ loading: true }); + const currentDashboard = await this.props.httpClient.get( + `/api/v1/dashboard/${this.state.currentDashboardSelector}` + ); + this.setState({ + currentDashboard, + loading: false + }); + } catch (e) { + this.setState({ + loading: false + }); + console.error(e); + } + }; + + init = async () => { + await this.getDashboards(); + if (this.state.currentDashboardSelector) { + await this.getCurrentDashboard(); + } + }; + + cancelDashboardEdit = async () => { + route(`/dashboard/${this.state.currentDashboardSelector}`); + }; + + moveCard = async (originalX, originalY, destX, destY) => { + // incorrect coordinates + if (destX < 0 || destY < 0) { + return null; + } + if (destX >= this.state.currentDashboard.boxes.length || destY >= this.state.currentDashboard.boxes[destX].length) { + return null; + } + const element = this.state.currentDashboard.boxes[originalX][originalY]; + const newStateWithoutElement = update(this.state, { + currentDashboard: { + boxes: { + [originalX]: { + $splice: [[originalY, 1]] + } + } + } + }); + const newState = update(newStateWithoutElement, { + currentDashboard: { + boxes: { + [destX]: { + $splice: [[destY, 0, element]] + } + } + } + }); + await this.setState(newState); + }; + + moveBoxDown = (x, y) => { + this.moveCard(x, y, x, y + 1); + }; + + moveBoxUp = (x, y) => { + this.moveCard(x, y, x, y - 1); + }; + + addBox = x => { + if (this.state.newSelectedBoxType && this.state.newSelectedBoxType[x]) { + const newState = update(this.state, { + currentDashboard: { + boxes: { + [x]: { + $push: [ + { + type: this.state.newSelectedBoxType[x] + } + ] + } + } + } + }); + this.setState(newState); + } + }; + + removeBox = (x, y) => { + const newState = update(this.state, { + currentDashboard: { + boxes: { + [x]: { + $splice: [[y, 1]] + } + } + } + }); + this.setState(newState); + }; + + updateCurrentDashboardName = e => { + const newState = update(this.state, { + currentDashboard: { + name: { + $set: e.target.value + } + } + }); + this.setState(newState); + }; + + updateBoxConfig = (x, y, data) => { + const newState = update(this.state, { + currentDashboard: { + boxes: { + [x]: { + [y]: { + $merge: data + } + } + } + } + }); + this.setState(newState); + }; + + updateNewSelectedBox = (x, type) => { + const newSelectedBoxType = Object.assign({}, this.state.newSelectedBoxType, { + [x]: type + }); + this.setState({ + newSelectedBoxType + }); + }; + + saveDashboard = async () => { + this.setState({ + loading: true, + dashboardValidationError: false, + dashboardAlreadyExistError: false, + unknownError: false + }); + try { + const { currentDashboard: selectedDashboard, dashboards } = this.state; + const { selector } = selectedDashboard; + + const currentDashboard = await this.props.httpClient.patch( + `/api/v1/dashboard/${selector}`, + this.state.currentDashboard + ); + + const currentDashboardIndex = dashboards.findIndex(d => d.selector === selector); + const updatedDashboards = update(dashboards, { + [currentDashboardIndex]: { + $set: currentDashboard + } + }); + + await this.setState({ + currentDashboard, + loading: false, + dashboards: updatedDashboards + }); + route(`/dashboard/${currentDashboard.selector}`); + } catch (e) { + if (e.response && e.response.status === 422) { + this.setState({ + dashboardValidationError: true + }); + } else if (e.response && e.response.status === 409) { + this.setState({ + dashboardAlreadyExistError: true + }); + } else { + this.setState({ + unknownError: true + }); + } + } + }; + + askDeleteCurrentDashboard = async () => { + await this.setState({ + askDeleteDashboard: true + }); + }; + + cancelDeleteCurrentDashboard = async () => { + await this.setState({ + askDeleteDashboard: false + }); + }; + + deleteCurrentDashboard = async () => { + try { + await this.props.httpClient.delete(`/api/v1/dashboard/${this.state.currentDashboard.selector}`); + const dashboardIndex = this.state.dashboards.findIndex(d => d.id === this.state.currentDashboard.id); + const dashboards = update(this.state.dashboards, { + $splice: [[dashboardIndex, 1]] + }); + const currentDashboard = dashboards.length > 0 ? dashboards[0] : null; + await this.setState({ + askDeleteDashboard: false + }); + route(`/dashboard/${currentDashboard.selector}/edit`); + } catch (e) { + console.error(e); + } + }; + + updateDashboardList = async newDashboards => { + await this.setState({ + savingNewDashboardList: true, + dashboards: newDashboards + }); + try { + const dashboardSelectors = this.state.dashboards.map(d => d.selector); + await this.props.httpClient.post('/api/v1/dashboard/order', dashboardSelectors); + } catch (e) { + console.error(e); + } + this.setState({ + savingNewDashboardList: false + }); + }; + + constructor(props) { + super(props); + this.props = props; + this.state = { + dashboards: [], + newSelectedBoxType: {}, + askDeleteDashboard: false + }; + } + + componentDidMount() { + this.init(); + } + + componentDidUpdate(prevProps) { + if (prevProps.currentUrl !== this.props.currentUrl) { + this.init(); + } + } + + render( + props, + { + dashboards, + currentDashboard, + loading, + dashboardValidationError, + dashboardAlreadyExistError, + unknownError, + askDeleteDashboard, + savingNewDashboardList + } + ) { + const dashboardConfigured = + currentDashboard && + currentDashboard.boxes && + ((currentDashboard.boxes[0] && currentDashboard.boxes[0].length > 0) || + (currentDashboard.boxes[1] && currentDashboard.boxes[1].length > 0) || + (currentDashboard.boxes[2] && currentDashboard.boxes[2].length > 0)); + const dashboardListEmpty = !(dashboards && dashboards.length > 0); + const dashboardNotConfigured = !dashboardConfigured; + return ( + + ); + } +} + +export default connect('user,fullScreen,currentUrl,httpClient,gatewayAccountExpired', {})(EditDashboard); diff --git a/front/src/routes/dashboard/index.js b/front/src/routes/dashboard/index.js index 8b3f40bf7c..9b8bba19ba 100644 --- a/front/src/routes/dashboard/index.js +++ b/front/src/routes/dashboard/index.js @@ -87,7 +87,7 @@ class Dashboard extends Component { }; editDashboard = () => { - this.setState(prevState => ({ ...prevState, dashboardEditMode: !prevState.dashboardEditMode })); + route(`/dashboard/${this.state.currentDashboard.selector}/edit`); }; cancelDashboardEdit = async () => { diff --git a/front/src/routes/dashboard/new-dashboard/index.js b/front/src/routes/dashboard/new-dashboard/index.js index 5f2da5ca87..ffec5ed749 100644 --- a/front/src/routes/dashboard/new-dashboard/index.js +++ b/front/src/routes/dashboard/new-dashboard/index.js @@ -1,17 +1,19 @@ import { Component } from 'preact'; import { Text, Localizer } from 'preact-i18n'; import { connect } from 'unistore/preact'; -import { Link } from 'preact-router/match'; import { route } from 'preact-router'; +import { Link } from 'preact-router/match'; import cx from 'classnames'; import { DASHBOARD_TYPE } from '../../../../../server/utils/constants'; import style from './style.css'; const NewDashboardPage = ({ children, ...props }) => (
      - - - + {props.prev && ( + + + + )}
      ); -@connect('user,httpClient', {}) class Dashboard extends Component { updateName = e => { this.setState({ name: e.target.value }); }; + goBack = () => { + this.props.history.go(-1); + }; createDashboard = async e => { e.preventDefault(); await this.setState({ @@ -88,7 +92,7 @@ class Dashboard extends Component { }; const createDashboard = await this.props.httpClient.post('/api/v1/dashboard', newDashboard); this.setState({ loading: false, dashboardAlreadyExistError: false, unknownError: false }); - route(`/dashboard/${createDashboard.selector}`); + route(`/dashboard/${createDashboard.selector}/edit`); } catch (e) { if (e.response && e.response.status === 409) { this.setState({ dashboardAlreadyExistError: true }); @@ -116,9 +120,11 @@ class Dashboard extends Component { unknownError={unknownError} updateName={this.updateName} createDashboard={this.createDashboard} + goBack={this.goBack} + prev={props.prev} /> ); } } -export default Dashboard; +export default connect('user,httpClient', {})(Dashboard); diff --git a/front/src/routes/dashboard/style.css b/front/src/routes/dashboard/style.css index 606fe648da..c93f7ac69d 100644 --- a/front/src/routes/dashboard/style.css +++ b/front/src/routes/dashboard/style.css @@ -19,6 +19,10 @@ margin-top: 20px; } +.largeContainer { + max-width: 1600px; +} + @media (min-width: 992px) { .removePaddingFirstCol { padding-left: 0; @@ -57,3 +61,13 @@ overflow: auto; transition: max-height 0.5s ease-in; } + +.editDashboardText { + display: inline; +} + +@media (max-width: 992px) { + .editDashboardText { + display: none; + } +} diff --git a/server/lib/dashboard/dashboard.create.js b/server/lib/dashboard/dashboard.create.js index 639972d63b..b8ed6b7732 100644 --- a/server/lib/dashboard/dashboard.create.js +++ b/server/lib/dashboard/dashboard.create.js @@ -13,6 +13,19 @@ const db = require('../../models'); * }); */ async function create(userId, dashboard) { + // We try to find if one dashboard already exist, if yes we use the position of this dashboard + 1 + const dashboardWithTheHighestPosition = await db.Dashboard.findAll({ + attributes: ['position'], + where: { + user_id: userId, + }, + order: [['position', 'desc']], + limit: 1, + raw: true, + }); + if (dashboardWithTheHighestPosition.length > 0) { + dashboard.position = dashboardWithTheHighestPosition[0].position + 1; + } return db.Dashboard.create({ ...dashboard, user_id: userId }); } diff --git a/server/models/dashboard.js b/server/models/dashboard.js index eee2d5a018..7f72ec4de2 100644 --- a/server/models/dashboard.js +++ b/server/models/dashboard.js @@ -58,6 +58,7 @@ module.exports = (sequelize, DataTypes) => { position: { allowNull: false, type: DataTypes.INTEGER, + defaultValue: 0, }, selector: { allowNull: false, diff --git a/server/test/controllers/dashboard/dashboard.controller.test.js b/server/test/controllers/dashboard/dashboard.controller.test.js index 54936bbea2..baf739500a 100644 --- a/server/test/controllers/dashboard/dashboard.controller.test.js +++ b/server/test/controllers/dashboard/dashboard.controller.test.js @@ -86,6 +86,7 @@ describe('PATCH /api/v1/dashboard/:dashboard_selector', () => { id: '854dda11-80c0-4476-843b-65cbc95c6a85', name: 'new name', selector: 'test-dashboard', + position: 0, user_id: '0cd30aef-9c4e-4a23-88e3-3547971296e5', type: 'main', boxes: [ From e6fada6deefd0dc1a2d89ad655aa2b7c84b948d1 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 3 Feb 2023 10:11:07 +0700 Subject: [PATCH 5/7] Improve empty state experience & fix Cypress tests --- .../cypress/e2e/routes/dashboard/Dashboard.cy.js | 15 +++++++++------ front/src/routes/dashboard/DashboardPage.jsx | 2 +- front/src/routes/dashboard/EmptyState.jsx | 6 ++++++ .../src/routes/dashboard/edit-dashboard/index.js | 6 +++++- front/src/routes/dashboard/new-dashboard/index.js | 5 +++++ 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/front/cypress/e2e/routes/dashboard/Dashboard.cy.js b/front/cypress/e2e/routes/dashboard/Dashboard.cy.js index 6b77ac2ddf..f48c4b0ea9 100644 --- a/front/cypress/e2e/routes/dashboard/Dashboard.cy.js +++ b/front/cypress/e2e/routes/dashboard/Dashboard.cy.js @@ -4,8 +4,10 @@ describe('Dashboard', () => { }); it('Should create new dashboard', () => { cy.visit('/dashboard'); - cy.contains('dashboard.newDashboardButton') - .should('have.class', 'btn-outline-success') + + cy.get('a') + .contains('dashboard.newDashboardButton') + .should('have.class', 'btn-success') .click(); cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard/create/new`); @@ -20,12 +22,9 @@ describe('Dashboard', () => { .should('have.class', 'btn-primary') .click(); - cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard/my-new-dashboard`); + cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard/my-new-dashboard/edit`); }); it('Should add new boxes', () => { - cy.contains('dashboard.editDashboardButton') - .should('have.class', 'btn-outline-primary') - .click(); cy.get('select').then(inputs => { cy.wrap(inputs[0]).select('user-presence'); cy.get('button').then(inputs => { @@ -51,5 +50,9 @@ describe('Dashboard', () => { cy.contains('dashboard.editDashboardDeleteButton') .should('have.class', 'btn-outline-danger') .click(); + cy.contains('dashboard.editDashboardDeleteButton') + .should('have.class', 'btn-outline-danger') + .click(); + cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard`); }); }); diff --git a/front/src/routes/dashboard/DashboardPage.jsx b/front/src/routes/dashboard/DashboardPage.jsx index 52fde98548..46d6dde074 100644 --- a/front/src/routes/dashboard/DashboardPage.jsx +++ b/front/src/routes/dashboard/DashboardPage.jsx @@ -56,7 +56,7 @@ const DashboardPage = ({ children, ...props }) => ( {' '} - + )}
      diff --git a/front/src/routes/dashboard/EmptyState.jsx b/front/src/routes/dashboard/EmptyState.jsx index 8df2c0db67..67d6de9182 100644 --- a/front/src/routes/dashboard/EmptyState.jsx +++ b/front/src/routes/dashboard/EmptyState.jsx @@ -1,4 +1,5 @@ import { Text } from 'preact-i18n'; +import { Link } from 'preact-router/match'; import style from './style.css'; const EmptyState = ({ children, ...props }) => ( @@ -9,6 +10,11 @@ const EmptyState = ({ children, ...props }) => (
      {!props.dashboardListEmpty && } {props.dashboardListEmpty && }

      + {props.dashboardListEmpty && ( + + + + )}
      ); diff --git a/front/src/routes/dashboard/edit-dashboard/index.js b/front/src/routes/dashboard/edit-dashboard/index.js index 5f756c240e..c3fa49adc3 100644 --- a/front/src/routes/dashboard/edit-dashboard/index.js +++ b/front/src/routes/dashboard/edit-dashboard/index.js @@ -246,7 +246,11 @@ class EditDashboard extends Component { await this.setState({ askDeleteDashboard: false }); - route(`/dashboard/${currentDashboard.selector}/edit`); + if (currentDashboard === null) { + route('/dashboard'); + } else { + route(`/dashboard/${currentDashboard.selector}/edit`); + } } catch (e) { console.error(e); } diff --git a/front/src/routes/dashboard/new-dashboard/index.js b/front/src/routes/dashboard/new-dashboard/index.js index ffec5ed749..a91c9211f9 100644 --- a/front/src/routes/dashboard/new-dashboard/index.js +++ b/front/src/routes/dashboard/new-dashboard/index.js @@ -14,6 +14,11 @@ const NewDashboardPage = ({ children, ...props }) => ( )} + {!props.prev && ( + + + + )}