diff --git a/client/package-lock.json b/client/package-lock.json index f30028c1..76955972 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -33,6 +33,7 @@ "react-router-dom": "^6.18.0", "react-select": "^5.2.2", "react-string-replace": "^1.1.0", + "react-tiny-popover": "^8.1.2", "react-to-print": "^2.14.12", "react-tooltip": "^5.22.0", "reactjs-popup": "^2.0.6", @@ -3866,15 +3867,6 @@ "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "dev": true, @@ -4346,9 +4338,10 @@ "license": "ISC" }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, - "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" @@ -4356,23 +4349,27 @@ }, "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.11.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.11.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "dev": true, - "license": "MIT" + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dev": true, - "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", @@ -4381,62 +4378,69 @@ }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.11.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "node_modules/@webassemblyjs/ieee754": { "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dev": true, - "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { "version": "1.11.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -4444,22 +4448,24 @@ } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -4468,23 +4474,26 @@ } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", - "dev": true, - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true }, "node_modules/@xtuc/long": { "version": "4.2.2", - "dev": true, - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true }, "node_modules/abab": { "version": "2.0.6", @@ -4539,10 +4548,11 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, - "license": "MIT", "peerDependencies": { "acorn": "^8" } @@ -5339,9 +5349,9 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -5352,7 +5362,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -5545,12 +5555,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "license": "MIT", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6028,9 +6044,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "engines": { "node": ">= 0.6" @@ -6801,15 +6817,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "license": "MIT", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -6844,8 +6864,9 @@ }, "node_modules/depd": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -6860,8 +6881,9 @@ }, "node_modules/destroy": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -7091,8 +7113,9 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true }, "node_modules/ejs": { "version": "3.1.10", @@ -7127,17 +7150,19 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -7226,6 +7251,25 @@ "dev": true, "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -8031,8 +8075,9 @@ }, "node_modules/etag": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -8085,37 +8130,37 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -8288,12 +8333,13 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, - "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -8306,16 +8352,18 @@ }, "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/find-cache-dir": { "version": "3.3.2", @@ -8632,8 +8680,9 @@ }, "node_modules/fresh": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -8721,14 +8770,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "license": "MIT", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8803,8 +8857,9 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", - "dev": true, - "license": "BSD-2-Clause" + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true }, "node_modules/global-modules": { "version": "2.0.0", @@ -8941,10 +8996,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9178,8 +9234,9 @@ }, "node_modules/http-errors": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, - "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -11884,9 +11941,13 @@ "license": "MIT" }, "node_modules/merge-descriptors": { - "version": "1.0.1", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, - "license": "MIT" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -11923,8 +11984,9 @@ }, "node_modules/mime": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, - "license": "MIT", "bin": { "mime": "cli.js" }, @@ -12389,8 +12451,9 @@ }, "node_modules/on-finished": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, - "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -12600,9 +12663,10 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "dev": true, - "license": "MIT" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true }, "node_modules/path-type": { "version": "4.0.0", @@ -14156,12 +14220,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -15968,6 +16032,15 @@ "loose-envify": "^1.1.0" } }, + "node_modules/react-tiny-popover": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/react-tiny-popover/-/react-tiny-popover-8.1.2.tgz", + "integrity": "sha512-zzG9hGBCNeSesZLLItNOiQvIU+/tLdLQ9y8cfSAtLQnOH7eoKVJflQzEng8w7rTblE+cV0/R6dFjjiZS1A4qTA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-to-print": { "version": "2.14.15", "license": "MIT", @@ -16391,9 +16464,10 @@ } }, "node_modules/rollup": { - "version": "2.79.1", + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, - "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -16707,9 +16781,10 @@ "license": "ISC" }, "node_modules/send": { - "version": "0.18.0", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, - "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -16731,21 +16806,33 @@ }, "node_modules/send/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, - "license": "MIT" + "engines": { + "node": ">= 0.8" + } }, "node_modules/send/node_modules/ms": { "version": "2.1.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/serialize-javascript": { "version": "6.0.1", @@ -16826,27 +16913,31 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, - "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/set-function-length": { - "version": "1.1.1", - "license": "MIT", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -16866,8 +16957,9 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true }, "node_modules/shallow-equal": { "version": "1.2.1", @@ -16899,13 +16991,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -17209,8 +17306,9 @@ }, "node_modules/statuses": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -17864,9 +17962,10 @@ } }, "node_modules/terser": { - "version": "5.24.0", + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -17881,15 +17980,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -18036,8 +18136,9 @@ }, "node_modules/toidentifier": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.6" } @@ -18350,8 +18451,9 @@ }, "node_modules/unpipe": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -18533,9 +18635,10 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, - "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -18562,33 +18665,33 @@ } }, "node_modules/webpack": { - "version": "5.89.0", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, - "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -21688,14 +21791,6 @@ "@types/json-schema": "*" } }, - "@types/eslint-scope": { - "version": "3.7.7", - "dev": true, - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "@types/estree": { "version": "1.0.5", "dev": true @@ -22019,7 +22114,9 @@ "version": "1.2.0" }, "@webassemblyjs/ast": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, "requires": { "@webassemblyjs/helper-numbers": "1.11.6", @@ -22028,18 +22125,26 @@ }, "@webassemblyjs/floating-point-hex-parser": { "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", "dev": true }, "@webassemblyjs/helper-api-error": { "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", "dev": true }, "@webassemblyjs/helper-buffer": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true }, "@webassemblyjs/helper-numbers": { "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dev": true, "requires": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", @@ -22049,20 +22154,26 @@ }, "@webassemblyjs/helper-wasm-bytecode": { "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", "dev": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "@webassemblyjs/ieee754": { "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" @@ -22070,6 +22181,8 @@ }, "@webassemblyjs/leb128": { "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "dev": true, "requires": { "@xtuc/long": "4.2.2" @@ -22077,27 +22190,33 @@ }, "@webassemblyjs/utf8": { "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", "dev": true }, "@webassemblyjs/wasm-edit": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "@webassemblyjs/wasm-gen": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -22105,20 +22224,24 @@ } }, "@webassemblyjs/wasm-opt": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "@webassemblyjs/wasm-parser": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -22127,19 +22250,25 @@ } }, "@webassemblyjs/wast-printer": { - "version": "1.11.6", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, "@xtuc/ieee754": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true }, "@xtuc/long": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, "abab": { @@ -22178,8 +22307,10 @@ } } }, - "acorn-import-assertions": { - "version": "1.9.0", + "acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true }, "acorn-jsx": { @@ -22695,9 +22826,9 @@ "dev": true }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "requires": { "bytes": "3.1.2", @@ -22708,7 +22839,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -22842,11 +22973,15 @@ "dev": true }, "call-bind": { - "version": "1.0.5", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "callsites": { @@ -23152,9 +23287,9 @@ "dev": true }, "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true }, "cookie-signature": { @@ -23631,11 +23766,13 @@ } }, "define-data-property": { - "version": "1.1.1", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "requires": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" } }, "define-lazy-prop": { @@ -23655,6 +23792,8 @@ }, "depd": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true }, "dequal": { @@ -23663,6 +23802,8 @@ }, "destroy": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true }, "detect-newline": { @@ -23824,6 +23965,8 @@ }, "ee-first": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, "ejs": { @@ -23847,11 +23990,15 @@ "dev": true }, "encodeurl": { - "version": "1.0.2", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true }, "enhanced-resolve": { - "version": "5.15.0", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -23924,6 +24071,19 @@ "version": "1.0.0", "dev": true }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -24433,6 +24593,8 @@ }, "etag": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true }, "eventemitter3": { @@ -24468,37 +24630,37 @@ "dev": true }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -24626,11 +24788,13 @@ } }, "finalhandler": { - "version": "1.2.0", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -24640,6 +24804,8 @@ "dependencies": { "debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { "ms": "2.0.0" @@ -24647,6 +24813,8 @@ }, "ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true } } @@ -24842,6 +25010,8 @@ }, "fresh": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true }, "fs-extra": { @@ -24892,8 +25062,11 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.2", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", @@ -24940,6 +25113,8 @@ }, "glob-to-regexp": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, "global-modules": { @@ -25026,9 +25201,11 @@ "version": "4.0.0" }, "has-property-descriptors": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "requires": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" } }, "has-proto": { @@ -25177,6 +25354,8 @@ }, "http-errors": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, "requires": { "depd": "2.0.0", @@ -27041,7 +27220,9 @@ "version": "5.2.1" }, "merge-descriptors": { - "version": "1.0.1", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true }, "merge-stream": { @@ -27066,6 +27247,8 @@ }, "mime": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true }, "mime-db": { @@ -27340,6 +27523,8 @@ }, "on-finished": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "requires": { "ee-first": "1.1.1" @@ -27469,7 +27654,9 @@ "version": "1.0.7" }, "path-to-regexp": { - "version": "0.1.7", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true }, "path-type": { @@ -28273,12 +28460,12 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "querystringify": { @@ -29639,6 +29826,11 @@ } } }, + "react-tiny-popover": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/react-tiny-popover/-/react-tiny-popover-8.1.2.tgz", + "integrity": "sha512-zzG9hGBCNeSesZLLItNOiQvIU+/tLdLQ9y8cfSAtLQnOH7eoKVJflQzEng8w7rTblE+cV0/R6dFjjiZS1A4qTA==" + }, "react-to-print": { "version": "2.14.15" }, @@ -29905,7 +30097,9 @@ } }, "rollup": { - "version": "2.79.1", + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -30089,7 +30283,9 @@ } }, "send": { - "version": "0.18.0", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "requires": { "debug": "2.6.9", @@ -30109,6 +30305,8 @@ "dependencies": { "debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { "ms": "2.0.0" @@ -30116,12 +30314,22 @@ "dependencies": { "ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true } } }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, "ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true } } @@ -30186,22 +30394,28 @@ } }, "serve-static": { - "version": "1.15.0", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" } }, "set-function-length": { - "version": "1.1.1", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "requires": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" } }, "set-function-name": { @@ -30214,6 +30428,8 @@ }, "setprototypeof": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, "shallow-equal": { @@ -30233,12 +30449,15 @@ "dev": true }, "side-channel": { - "version": "1.0.4", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { @@ -30449,6 +30668,8 @@ }, "statuses": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true }, "stop-iteration-iterator": { @@ -30880,7 +31101,9 @@ } }, "terser": { - "version": "5.24.0", + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", "dev": true, "requires": { "@jridgewell/source-map": "^0.3.3", @@ -30896,14 +31119,16 @@ } }, "terser-webpack-plugin": { - "version": "5.3.9", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" } }, "test-exclude": { @@ -30989,6 +31214,8 @@ }, "toidentifier": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, "toposort": { @@ -31206,6 +31433,8 @@ }, "unpipe": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true }, "unquote": { @@ -31317,7 +31546,9 @@ } }, "watchpack": { - "version": "2.4.0", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "requires": { "glob-to-regexp": "^0.4.1", @@ -31338,32 +31569,33 @@ "dev": true }, "webpack": { - "version": "5.89.0", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "requires": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" } }, diff --git a/client/package.json b/client/package.json index 37aad54f..f5c43ffc 100644 --- a/client/package.json +++ b/client/package.json @@ -54,6 +54,7 @@ "react-router-dom": "^6.18.0", "react-select": "^5.2.2", "react-string-replace": "^1.1.0", + "react-tiny-popover": "^8.1.2", "react-to-print": "^2.14.12", "react-tooltip": "^5.22.0", "reactjs-popup": "^2.0.6", diff --git a/client/src/App.js b/client/src/App.js index 5fbc354e..20ed8591 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -33,6 +33,7 @@ import FaqView from "./components/Faq/FaqView"; import ResetPassword from "./components/Authorization/ResetPassword"; import ForgotPassword from "./components/Authorization/ForgotPassword"; import Feedback from "./components/Feedback/FeedbackPage"; +import SubmitSnapshotPage from "./components/SubmitSnapshot/SubmitSnapshotPage"; import ErrorPage from "./components/ErrorPage"; import Offline from "./components/Offline"; import Logout from "./components/Authorization/Logout"; @@ -144,6 +145,14 @@ const App = () => { path="/feedback" element={} /> + + + + } + /> } /> diff --git a/client/src/components/About.js b/client/src/components/About.jsx similarity index 100% rename from client/src/components/About.js rename to client/src/components/About.jsx diff --git a/client/src/components/Admin/Admin.js b/client/src/components/Admin/Admin.jsx similarity index 100% rename from client/src/components/Admin/Admin.js rename to client/src/components/Admin/Admin.jsx diff --git a/client/src/components/Admin/RuleFunction.js b/client/src/components/Admin/RuleFunction.jsx similarity index 100% rename from client/src/components/Admin/RuleFunction.js rename to client/src/components/Admin/RuleFunction.jsx diff --git a/client/src/components/Admin/RuleView.js b/client/src/components/Admin/RuleView.jsx similarity index 100% rename from client/src/components/Admin/RuleView.js rename to client/src/components/Admin/RuleView.jsx diff --git a/client/src/components/Admin/RuleViewContainer.js b/client/src/components/Admin/RuleViewContainer.js deleted file mode 100644 index ab60539e..00000000 --- a/client/src/components/Admin/RuleViewContainer.js +++ /dev/null @@ -1,38 +0,0 @@ -import React, { useState } from "react"; -import PropTypes from "prop-types"; -import RuleView from "./RuleView"; - -const RuleViewContainer = props => { - const { rules } = props; - const [ruleId, setRuleId] = useState(rules[0].id); - const rule = rules.filter(rule => rule.id === ruleId)[0]; - - return ( - - - - - ); -}; -RuleViewContainer.propTypes = { - rules: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.number, - filter: PropTypes.string.isRequired, - length: PropTypes.number.isRequired - }) - ) -}; - -export default RuleViewContainer; diff --git a/client/src/components/Admin/RuleViewContainer.jsx b/client/src/components/Admin/RuleViewContainer.jsx new file mode 100644 index 00000000..1b17ea83 --- /dev/null +++ b/client/src/components/Admin/RuleViewContainer.jsx @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import RuleView from "./RuleView"; +import UniversalSelect from "../UI/UniversalSelect"; +import { createUseStyles } from "react-jss"; + +const useStyles = createUseStyles({ + select: { + width: "275px" + } +}); + +const RuleViewContainer = props => { + const { rules } = props; + const [ruleId, setRuleId] = useState(rules[0].id); + const rule = rules.filter(rule => rule.id === ruleId)[0]; + const classes = useStyles(); + + const handleSetRule = e => { + const ruleId = Number(e.target.value); + setRuleId(ruleId); + }; + + return ( + + ({ + value: rule.id.toString(), + label: rule.code + }))} + value={ruleId} + onChange={handleSetRule} + name={"Rule"} + /> + + + + ); +}; +RuleViewContainer.propTypes = { + rules: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number, + // filter: PropTypes.string.isRequired, + // length: PropTypes.number.isRequired, + code: PropTypes.string.isRequired + }) + ) +}; + +export default RuleViewContainer; diff --git a/client/src/components/ArchiveDelete/ProjectsArchive.js b/client/src/components/ArchiveDelete/ProjectsArchive.jsx similarity index 100% rename from client/src/components/ArchiveDelete/ProjectsArchive.js rename to client/src/components/ArchiveDelete/ProjectsArchive.jsx diff --git a/client/src/components/ArchiveDelete/RolesArchive.js b/client/src/components/ArchiveDelete/RolesArchive.jsx similarity index 100% rename from client/src/components/ArchiveDelete/RolesArchive.js rename to client/src/components/ArchiveDelete/RolesArchive.jsx diff --git a/client/src/components/ArchiveDelete/RolesContextMenu.js b/client/src/components/ArchiveDelete/RolesContextMenu.jsx similarity index 100% rename from client/src/components/ArchiveDelete/RolesContextMenu.js rename to client/src/components/ArchiveDelete/RolesContextMenu.jsx diff --git a/client/src/components/ArchiveDelete/RolesDeleteContextMenu.js b/client/src/components/ArchiveDelete/RolesDeleteContextMenu.jsx similarity index 100% rename from client/src/components/ArchiveDelete/RolesDeleteContextMenu.js rename to client/src/components/ArchiveDelete/RolesDeleteContextMenu.jsx diff --git a/client/src/components/ArchiveDelete/RolesUnarchiveContextMenu.js b/client/src/components/ArchiveDelete/RolesUnarchiveContextMenu.jsx similarity index 100% rename from client/src/components/ArchiveDelete/RolesUnarchiveContextMenu.js rename to client/src/components/ArchiveDelete/RolesUnarchiveContextMenu.jsx diff --git a/client/src/components/Authorization/ConfirmEmail.js b/client/src/components/Authorization/ConfirmEmail.jsx similarity index 100% rename from client/src/components/Authorization/ConfirmEmail.js rename to client/src/components/Authorization/ConfirmEmail.jsx diff --git a/client/src/components/Authorization/ForgotPassword.js b/client/src/components/Authorization/ForgotPassword.jsx similarity index 100% rename from client/src/components/Authorization/ForgotPassword.js rename to client/src/components/Authorization/ForgotPassword.jsx diff --git a/client/src/components/Authorization/Login.js b/client/src/components/Authorization/Login.jsx similarity index 99% rename from client/src/components/Authorization/Login.js rename to client/src/components/Authorization/Login.jsx index 8444e5e2..5de97046 100644 --- a/client/src/components/Authorization/Login.js +++ b/client/src/components/Authorization/Login.jsx @@ -181,7 +181,7 @@ const Login = () => { > + + ); +}; + +SubmitButton.propTypes = { + children: PropTypes.object, + onClick: PropTypes.func.isRequired, + id: PropTypes.string.isRequired, + color: PropTypes.string, + isDisabled: PropTypes.bool, + isDisplayed: PropTypes.bool.isRequired +}; + +export default SubmitButton; diff --git a/client/src/components/Checklist/ChecklistContent.js b/client/src/components/Checklist/ChecklistContent.jsx similarity index 100% rename from client/src/components/Checklist/ChecklistContent.js rename to client/src/components/Checklist/ChecklistContent.jsx diff --git a/client/src/components/Checklist/ChecklistModal.js b/client/src/components/Checklist/ChecklistModal.jsx similarity index 94% rename from client/src/components/Checklist/ChecklistModal.js rename to client/src/components/Checklist/ChecklistModal.jsx index fd7dc346..fca12d3f 100644 --- a/client/src/components/Checklist/ChecklistModal.js +++ b/client/src/components/Checklist/ChecklistModal.jsx @@ -3,7 +3,7 @@ import { createUseStyles } from "react-jss"; import Modal from "react-modal"; import PropTypes from "prop-types"; import ChecklistContent from "./ChecklistContent"; -import { FaX } from "react-icons/fa6"; +import { MdClose } from "react-icons/md"; import "./ChecklistModal.css"; @@ -68,8 +68,7 @@ const ChecklistModal = ({ checklistModalOpen, toggleChecklistModal }) => { className={classes.modal} > - {/* */} - + diff --git a/client/src/components/Checklist/ChecklistPage.js b/client/src/components/Checklist/ChecklistPage.jsx similarity index 100% rename from client/src/components/Checklist/ChecklistPage.js rename to client/src/components/Checklist/ChecklistPage.jsx diff --git a/client/src/components/ErrorPage.js b/client/src/components/ErrorPage.jsx similarity index 100% rename from client/src/components/ErrorPage.js rename to client/src/components/ErrorPage.jsx diff --git a/client/src/components/Faq/Answer.jsx b/client/src/components/Faq/Answer.jsx index d604a5a9..28206de3 100644 --- a/client/src/components/Faq/Answer.jsx +++ b/client/src/components/Faq/Answer.jsx @@ -3,6 +3,7 @@ import Quill from "../UI/Quill"; import { createUseStyles } from "react-jss"; import PropTypes from "prop-types"; import { Interweave } from "interweave"; +import { MdLaunch } from "react-icons/md"; const useStyles = createUseStyles(theme => ({ answerContainer: { @@ -22,6 +23,11 @@ const useStyles = createUseStyles(theme => ({ "&:hover": { textDecoration: admin => admin && "underline" } + }, + externalLinkIcon: { + fontSize: "14px", + padding: " 0 0.4em", + color: "#00F" } })); @@ -78,7 +84,7 @@ export const Answer = ({ style={{ display: "flex" }} >
- +
)} @@ -94,3 +100,17 @@ Answer.propTypes = { setIsEditAnswer: PropTypes.func, onSetFaq: PropTypes.func }; + +function TransformExternalLink(node, children) { + const classes = useStyles(); + if (node.tagName == "A" && !node.getAttribute("href").startsWith("/")) { + return ( + + + {children} + + + + ); + } +} diff --git a/client/src/components/Faq/DeleteFaqModal.jsx b/client/src/components/Faq/DeleteFaqModal.jsx index 69f4a57c..0a667c8f 100644 --- a/client/src/components/Faq/DeleteFaqModal.jsx +++ b/client/src/components/Faq/DeleteFaqModal.jsx @@ -2,7 +2,7 @@ import React from "react"; import PropTypes from "prop-types"; import ModalDialog from "../UI/AriaModal/ModalDialog"; import Button from "../Button/Button"; -import WarningIcon from "../../images/warning-icon.png"; +import { MdWarning } from "react-icons/md"; import { createUseStyles } from "react-jss"; import { MdDelete } from "react-icons/md"; @@ -39,11 +39,11 @@ const DeleteFaqModal = ({ isModalOpen, closeModal, handleDelete, isFaq }) => { {` Delete ${type}`}
- Warning + {`Are you sure you want to permanently delete the ${type}?`}
- -
diff --git a/client/src/components/Faq/Faq.js b/client/src/components/Faq/Faq.jsx similarity index 100% rename from client/src/components/Faq/Faq.js rename to client/src/components/Faq/Faq.jsx diff --git a/client/src/components/Faq/FaqButtonContainer.jsx b/client/src/components/Faq/FaqButtonContainer.jsx index 9f6d76b8..bca26c64 100644 --- a/client/src/components/Faq/FaqButtonContainer.jsx +++ b/client/src/components/Faq/FaqButtonContainer.jsx @@ -51,12 +51,12 @@ export const FaqButtonContainer = ({ )}
{faq.expand ? ( - collapseFaq(faq)} /> ) : ( - expandFaq(faq)} /> diff --git a/client/src/components/Faq/FaqCategory.js b/client/src/components/Faq/FaqCategory.jsx similarity index 100% rename from client/src/components/Faq/FaqCategory.js rename to client/src/components/Faq/FaqCategory.jsx diff --git a/client/src/components/Faq/FaqCategoryList.js b/client/src/components/Faq/FaqCategoryList.jsx similarity index 100% rename from client/src/components/Faq/FaqCategoryList.js rename to client/src/components/Faq/FaqCategoryList.jsx diff --git a/client/src/components/Faq/SaveConfirmationModal.jsx b/client/src/components/Faq/SaveConfirmationModal.jsx index 5b8fda81..4fc54f4c 100644 --- a/client/src/components/Faq/SaveConfirmationModal.jsx +++ b/client/src/components/Faq/SaveConfirmationModal.jsx @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import ModalDialog from "../UI/AriaModal/ModalDialog"; import Button from "../Button/Button"; import { createUseStyles } from "react-jss"; -import WarningIcon from "../../images/warning-icon.png"; +import { MdWarning } from "react-icons/md"; import { MdSave } from "react-icons/md"; const useStyles = createUseStyles(theme => ({ @@ -38,11 +38,11 @@ const SaveConfirmationModal = ({ isOpen, onClose, onYes }) => { {" Save Edits"}
- Warning + {"Are you sure you want to save FAQ page edits?"}
- + +
+ + + + ); +}; + +AffordableEdgeCaseModal.propTypes = { + isOpen: PropTypes.bool, + onClose: PropTypes.func, + onYes: PropTypes.func +}; + +export default AffordableEdgeCaseModal; diff --git a/client/src/components/ProjectWizard/Common/Panels.js b/client/src/components/ProjectWizard/Common/Panels.jsx similarity index 100% rename from client/src/components/ProjectWizard/Common/Panels.js rename to client/src/components/ProjectWizard/Common/Panels.jsx diff --git a/client/src/components/ProjectWizard/InapplicableStrategiesModal.js b/client/src/components/ProjectWizard/InapplicableStrategiesModal.jsx similarity index 100% rename from client/src/components/ProjectWizard/InapplicableStrategiesModal.js rename to client/src/components/ProjectWizard/InapplicableStrategiesModal.jsx diff --git a/client/src/components/ProjectWizard/InfoBox.js b/client/src/components/ProjectWizard/InfoBox.jsx similarity index 100% rename from client/src/components/ProjectWizard/InfoBox.js rename to client/src/components/ProjectWizard/InfoBox.jsx diff --git a/client/src/components/ProjectWizard/NavConfirmDialog.js b/client/src/components/ProjectWizard/NavConfirmDialog.jsx similarity index 92% rename from client/src/components/ProjectWizard/NavConfirmDialog.js rename to client/src/components/ProjectWizard/NavConfirmDialog.jsx index aa7400c7..6be4abb5 100644 --- a/client/src/components/ProjectWizard/NavConfirmDialog.js +++ b/client/src/components/ProjectWizard/NavConfirmDialog.jsx @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import ModalDialog from "../UI/AriaModal/ModalDialog"; import Button from "../Button/Button"; import { createUseStyles } from "react-jss"; -import WarningIcon from "../../images/warning-icon.png"; +import { MdWarning } from "react-icons/md"; const useStyles = createUseStyles({ title: { @@ -37,7 +37,7 @@ const NavConfirmDialog = ({ blocker }) => {

- Warning +   This will permanently delete any unsaved projects or changes to project. @@ -46,7 +46,7 @@ const NavConfirmDialog = ({ blocker }) => {

@@ -163,9 +171,10 @@ WizardFooter.propTypes = { setDisabledSaveButton: PropTypes.any, setDisplaySaveButton: PropTypes.any, setDisplayPrintButton: PropTypes.any, + setDisplaySubmitButton: PropTypes.any, onSave: PropTypes.any, onDownload: PropTypes.any, - project: PropTypes.shape + project: PropTypes.any }; export default WizardFooter; diff --git a/client/src/components/ProjectWizard/WizardPages/DiscoverTooltips.js b/client/src/components/ProjectWizard/WizardPages/DiscoverTooltips.jsx similarity index 86% rename from client/src/components/ProjectWizard/WizardPages/DiscoverTooltips.js rename to client/src/components/ProjectWizard/WizardPages/DiscoverTooltips.jsx index 28acadb4..348977c8 100644 --- a/client/src/components/ProjectWizard/WizardPages/DiscoverTooltips.js +++ b/client/src/components/ProjectWizard/WizardPages/DiscoverTooltips.jsx @@ -1,12 +1,13 @@ import React from "react"; import { createUseStyles } from "react-jss"; +import { jssTheme } from "../../../styles/theme"; const useStyles = createUseStyles({ container: { width: "calc((100% / 14.5) * 3);", - background: "#e9e9f1", + background: jssTheme.colors.secondary.lightGray, padding: "1em", - paddingBottom: "0", + paddingBottom: "1em", position: "absolute", left: "23.5px", textAlign: "initial" diff --git a/client/src/components/ProjectWizard/WizardPages/Level0Page.jsx b/client/src/components/ProjectWizard/WizardPages/Level0Page.jsx index 441e1b0b..20987978 100644 --- a/client/src/components/ProjectWizard/WizardPages/Level0Page.jsx +++ b/client/src/components/ProjectWizard/WizardPages/Level0Page.jsx @@ -1,9 +1,9 @@ import React from "react"; import PropTypes from "prop-types"; import PlanningIcon from "../../../images/planning.png"; -import WarningIcon from "../../../images/warning-icon.png"; -import { createUseStyles } from "react-jss"; +import { createUseStyles, useTheme } from "react-jss"; import { MdLaunch } from "react-icons/md"; +import { MdWarning } from "react-icons/md"; const useStyles = createUseStyles({ level0NavButtons: { @@ -12,21 +12,13 @@ const useStyles = createUseStyles({ } }, level0Container: { - textAlign: "center", - - "& h1": { - fontFamily: "Oswald", - fontWeight: "bold", - fontSize: "30px", - lineHeight: "44px", - marginTop: "22px" - } + textAlign: "center" }, level0Message: { marginTop: "20px", maxWidth: "800px", - backgroundColor: "#FEF4F2", - color: "#B64E38", + backgroundColor: "white", + color: "black", fontSize: "22px", lineHeight: "38px", padding: "60px 48px 40px", @@ -43,7 +35,8 @@ const useStyles = createUseStyles({ }); const Level0Page = ({ isLevel0 }) => { - const classes = useStyles(); + const theme = useTheme(); + const classes = useStyles({ theme }); return ( <> @@ -51,13 +44,9 @@ const Level0Page = ({ isLevel0 }) => {
planningIcon -

Your project level is 0!

+

Your project level is 0!

- warningIcon +

Based on the information you provided, the Transportation Demand Management (TDM) Ordinance may not apply to diff --git a/client/src/components/ProjectWizard/WizardPages/ProjectDescriptions.js b/client/src/components/ProjectWizard/WizardPages/ProjectDescriptions.jsx similarity index 100% rename from client/src/components/ProjectWizard/WizardPages/ProjectDescriptions.js rename to client/src/components/ProjectWizard/WizardPages/ProjectDescriptions.jsx diff --git a/client/src/components/ProjectWizard/WizardPages/ProjectMeasures.js b/client/src/components/ProjectWizard/WizardPages/ProjectMeasures.jsx similarity index 91% rename from client/src/components/ProjectWizard/WizardPages/ProjectMeasures.js rename to client/src/components/ProjectWizard/WizardPages/ProjectMeasures.jsx index f2452d57..942ef2c8 100644 --- a/client/src/components/ProjectWizard/WizardPages/ProjectMeasures.js +++ b/client/src/components/ProjectWizard/WizardPages/ProjectMeasures.jsx @@ -33,8 +33,7 @@ const useStyles = createUseStyles({ boxShadow: "2px 2px 4px 2px rgba(0, 0, 0, 0.1)", display: "flex", alignItems: "center", - justifyContent: "center", - textShadow: "0px 4px 4px rgba(0,0,0,.25)" + justifyContent: "center" }, packageBannerIcon: { fontSize: "24px", @@ -48,6 +47,7 @@ const useStyles = createUseStyles({ }); function ProjectMeasure(props) { const { + projectLevel, rules, onInputChange, onCommentChange, @@ -74,14 +74,16 @@ function ProjectMeasure(props) { Select TDM Strategies

- Select TDM strategies to earn earn points to reach the Target (left - panel). + Select TDM strategies to earn points to reach the Target (left panel).
{(allowResidentialPackage || allowSchoolPackage) && ( <>
-
+
You qualify for a bonus package to earn 1 extra point!
@@ -105,6 +107,7 @@ function ProjectMeasure(props) { /> )} r.calculationPanelId != 27)} onInputChange={onInputChange} onCommentChange={onCommentChange} @@ -113,6 +116,7 @@ function ProjectMeasure(props) { ); } ProjectMeasure.propTypes = { + projectLevel: PropTypes.number, rules: PropTypes.arrayOf( PropTypes.shape({ calculationPanelId: PropTypes.number.isRequired, diff --git a/client/src/components/ProjectWizard/WizardPages/ProjectSpecifications.js b/client/src/components/ProjectWizard/WizardPages/ProjectSpecifications.jsx similarity index 100% rename from client/src/components/ProjectWizard/WizardPages/ProjectSpecifications.js rename to client/src/components/ProjectWizard/WizardPages/ProjectSpecifications.jsx diff --git a/client/src/components/ProjectWizard/WizardPages/ProjectSummary/PointsEarnedMessage.jsx b/client/src/components/ProjectWizard/WizardPages/ProjectSummary/PointsEarnedMessage.jsx index 7e948c5a..af43802c 100644 --- a/client/src/components/ProjectWizard/WizardPages/ProjectSummary/PointsEarnedMessage.jsx +++ b/client/src/components/ProjectWizard/WizardPages/ProjectSummary/PointsEarnedMessage.jsx @@ -1,15 +1,15 @@ import React from "react"; import PropTypes from "prop-types"; -import { createUseStyles } from "react-jss"; +import { createUseStyles, useTheme } from "react-jss"; import clsx from "clsx"; import { MdCheckCircle, MdWarning } from "react-icons/md"; const useStyles = createUseStyles({ success: { - color: "#A7C539" + color: ({ theme }) => theme.colorPrimary }, failure: { - color: "#E46247" + color: ({ theme }) => theme.colors.notice }, targetPointsReachedContainer: { display: "flex" @@ -17,13 +17,7 @@ const useStyles = createUseStyles({ targetPointsReached: { display: "flex", justifyContent: "center", - width: "100%", - textAlign: "center", - color: "#0F2940", - fontFamily: "Calibri", - fontSize: "16px", - lineHeight: "29px", - textShadow: "0px 4px 4px rgba(0,0,0,.25)" + width: "100%" }, messageBox: { display: "flex", @@ -41,15 +35,19 @@ const useStyles = createUseStyles({ }); const PointsEarnedMessage = props => { - const classes = useStyles(); + const theme = useTheme(); + const classes = useStyles({ theme }); const { targetPointsReached } = props; return (
{targetPointsReached ? ( -
- {" "} +
+
You have successfully earned the target points.
@@ -57,11 +55,14 @@ const PointsEarnedMessage = props => { ) : ( -
+
- You have not reached the target points.
- Please, go back and review your strategies + You have not reached the target points. Please, go back and review + your strategies.
diff --git a/client/src/components/ProjectWizard/WizardPages/ProjectSummary/ProjectSummary.js b/client/src/components/ProjectWizard/WizardPages/ProjectSummary/ProjectSummary.jsx similarity index 100% rename from client/src/components/ProjectWizard/WizardPages/ProjectSummary/ProjectSummary.js rename to client/src/components/ProjectWizard/WizardPages/ProjectSummary/ProjectSummary.jsx diff --git a/client/src/components/ProjectWizard/WizardPages/ProjectTargetPoints.js b/client/src/components/ProjectWizard/WizardPages/ProjectTargetPoints.jsx similarity index 98% rename from client/src/components/ProjectWizard/WizardPages/ProjectTargetPoints.js rename to client/src/components/ProjectWizard/WizardPages/ProjectTargetPoints.jsx index 472eb3ca..4093b2f1 100644 --- a/client/src/components/ProjectWizard/WizardPages/ProjectTargetPoints.js +++ b/client/src/components/ProjectWizard/WizardPages/ProjectTargetPoints.jsx @@ -2,7 +2,7 @@ import React from "react"; import PropTypes from "prop-types"; import { createUseStyles, useTheme } from "react-jss"; import RuleCalculationPanels from "../RuleCalculation/RuleCalculationPanels"; -import Level0Page from "../WizardPages/Level0Page"; +import Level0Page from "./Level0Page"; import ParkingProvidedRuleInput from "../RuleInput/ParkingProvidedRuleInput"; const useStyles = createUseStyles(theme => ({ diff --git a/client/src/components/ProjectWizard/WizardSidebar/EarnedPointsMetContainer.js b/client/src/components/ProjectWizard/WizardSidebar/EarnedPointsMetContainer.jsx similarity index 100% rename from client/src/components/ProjectWizard/WizardSidebar/EarnedPointsMetContainer.js rename to client/src/components/ProjectWizard/WizardSidebar/EarnedPointsMetContainer.jsx diff --git a/client/src/components/ProjectWizard/WizardSidebar/EarnedPointsProgress.js b/client/src/components/ProjectWizard/WizardSidebar/EarnedPointsProgress.jsx similarity index 100% rename from client/src/components/ProjectWizard/WizardSidebar/EarnedPointsProgress.js rename to client/src/components/ProjectWizard/WizardSidebar/EarnedPointsProgress.jsx diff --git a/client/src/components/ProjectWizard/WizardSidebar/SidebarCart.js b/client/src/components/ProjectWizard/WizardSidebar/SidebarCart.jsx similarity index 92% rename from client/src/components/ProjectWizard/WizardSidebar/SidebarCart.js rename to client/src/components/ProjectWizard/WizardSidebar/SidebarCart.jsx index 60e4468d..61490cc7 100644 --- a/client/src/components/ProjectWizard/WizardSidebar/SidebarCart.js +++ b/client/src/components/ProjectWizard/WizardSidebar/SidebarCart.jsx @@ -1,6 +1,6 @@ import React from "react"; import PropTypes from "prop-types"; -import { createUseStyles } from "react-jss"; +import { createUseStyles, useTheme } from "react-jss"; const useStyles = createUseStyles({ container: { @@ -56,7 +56,8 @@ const useStyles = createUseStyles({ const SidebarCart = props => { const { strategyRules, page } = props; - const classes = useStyles(); + const theme = useTheme(); + const classes = useStyles({ theme }); const reducedParkingRule = strategyRules.find( r => r.code === "STRATEGY_PARKING_5" ); @@ -105,7 +106,10 @@ const SidebarCart = props => { ))} {showReducedParkingNotification && ( -
+
{`Automatically earned ${reducedParkingRule.calcValue.toString()} points on the TDM Strategy Reduced Parking Supply for reducing parking by ${reductionPercentageText} below the Baseline.`} diff --git a/client/src/components/ProjectWizard/WizardSidebar/SidebarPoints.js b/client/src/components/ProjectWizard/WizardSidebar/SidebarPoints.jsx similarity index 100% rename from client/src/components/ProjectWizard/WizardSidebar/SidebarPoints.js rename to client/src/components/ProjectWizard/WizardSidebar/SidebarPoints.jsx diff --git a/client/src/components/ProjectWizard/WizardSidebar/SidebarPointsPanel.js b/client/src/components/ProjectWizard/WizardSidebar/SidebarPointsPanel.jsx similarity index 100% rename from client/src/components/ProjectWizard/WizardSidebar/SidebarPointsPanel.js rename to client/src/components/ProjectWizard/WizardSidebar/SidebarPointsPanel.jsx diff --git a/client/src/components/ProjectWizard/WizardSidebar/SidebarProjectLevel.js b/client/src/components/ProjectWizard/WizardSidebar/SidebarProjectLevel.jsx similarity index 100% rename from client/src/components/ProjectWizard/WizardSidebar/SidebarProjectLevel.js rename to client/src/components/ProjectWizard/WizardSidebar/SidebarProjectLevel.jsx diff --git a/client/src/components/ProjectWizard/WizardSidebar/WizardSidebar.js b/client/src/components/ProjectWizard/WizardSidebar/WizardSidebar.jsx similarity index 100% rename from client/src/components/ProjectWizard/WizardSidebar/WizardSidebar.js rename to client/src/components/ProjectWizard/WizardSidebar/WizardSidebar.jsx diff --git a/client/src/components/Projects/ColumnHeaderPopups/DatePopup.jsx b/client/src/components/Projects/ColumnHeaderPopups/DatePopup.jsx new file mode 100644 index 00000000..6d5b4156 --- /dev/null +++ b/client/src/components/Projects/ColumnHeaderPopups/DatePopup.jsx @@ -0,0 +1,126 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import Button from "../../Button/Button"; +import RadioButton from "../../UI/RadioButton"; +import "react-datepicker/dist/react-datepicker.css"; +import DateRangePicker from "../../UI/DateRangePicker"; +import { MdClose } from "react-icons/md"; + +const DatePopup = ({ + close, + header, + criteria, + setCriteria, + order, + orderBy, + setSort, + setCheckedProjectIds, + setSelectAllChecked +}) => { + const [newOrder, setNewOrder] = useState( + header.id !== orderBy ? null : order + ); + const [newStartDate, setNewStartDate] = useState( + criteria[header.startDatePropertyName] + ); + const [newEndDate, setNewEndDate] = useState( + criteria[header.endDatePropertyName] + ); + + const setDefault = () => { + setNewStartDate(null); + setNewEndDate(null); + setNewOrder(null); + setCheckedProjectIds([]); + setSelectAllChecked(false); + }; + + const applyChanges = () => { + setCriteria({ + ...criteria, + [header.startDatePropertyName]: newStartDate, + [header.endDatePropertyName]: newEndDate + }); + if (newOrder) { + setSort(header.id, newOrder); + } + setCheckedProjectIds([]); + setSelectAllChecked(false); + close(); + }; + + return ( +
+
+ +
+
+ setNewOrder("desc")} + /> + setNewOrder("asc")} + /> +
+
+
+ { + setNewStartDate(date); + }} + setEndDate={date => { + setNewEndDate(date); + }} + startDatePlaceholder="Start Date" + endDatePlaceholder="End Date" + /> +
+ +
+
+ + +
+
+ ); +}; + +DatePopup.propTypes = { + close: PropTypes.func, + header: PropTypes.any, + criteria: PropTypes.any, + setCriteria: PropTypes.func, + order: PropTypes.string, + orderBy: PropTypes.string, + setSort: PropTypes.func, + setCheckedProjectIds: PropTypes.func, + setSelectAllChecked: PropTypes.func +}; + +export default DatePopup; diff --git a/client/src/components/Projects/ColumnHeaderPopups/MultiSelectText.jsx b/client/src/components/Projects/ColumnHeaderPopups/MultiSelectText.jsx new file mode 100644 index 00000000..f19be38a --- /dev/null +++ b/client/src/components/Projects/ColumnHeaderPopups/MultiSelectText.jsx @@ -0,0 +1,120 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import SearchIcon from "../../../images/search.png"; +import { MdOutlineSearch } from "react-icons/md"; +import { createUseStyles } from "react-jss"; + +const useStyles = createUseStyles({ + searchBarWrapper: { + position: "relative", + alignSelf: "center", + marginBottom: "0.5rem" + }, + searchBar: { + maxWidth: "100%", + width: "20em", + padding: "12px 12px 12px 48px", + marginRight: "0.5rem" + }, + searchIcon: { + position: "absolute", + left: "16px", + top: "14px" + }, + listItem: { + display: "flex", + flexDirection: "row", + alignItems: "center", + height: "2rem", + "&:hover": { + backgroundColor: "lightblue" + } + } +}); + +const MultiSelectText = ({ options, selectedOptions, setSelectedOptions }) => { + const classes = useStyles(); + const [searchString, setSearchString] = useState(""); + const [selectedListItems, setSelectedListItems] = useState([ + ...selectedOptions + ]); + + const filteredOptions = options + .filter(o => !!o) + .filter(opt => opt.toLowerCase().includes(searchString.toLowerCase())); + + const onChangeSearchString = e => { + setSearchString(e.target.value); + }; + + const handleCheckboxChange = e => { + const optionValue = e.target.name; + if (selectedListItems.find(so => so.value == optionValue)) { + const newSelectedListItems = selectedListItems.filter( + selectedOption => selectedOption.value !== optionValue + ); + setSelectedListItems(newSelectedListItems); + setSelectedOptions(newSelectedListItems); + } else { + const newSelectedListItems = [ + ...selectedListItems, + { value: optionValue, label: optionValue } + ]; + setSelectedListItems(newSelectedListItems); + setSelectedOptions(newSelectedListItems); + } + }; + + const isChecked = optionValue => { + const checked = selectedListItems.find( + option => option.value == optionValue + ); + return !!checked; + }; + + return ( + <> +
+ + + Search Icon +
+ +
+
{JSON.stringify(selectedOptions, null, 2)}
+ {/*
{JSON.stringify(options, null, 2)}
*/} + + {filteredOptions.map(o => ( +
+ + {o} +
+ ))} +
+ + ); +}; + +MultiSelectText.propTypes = { + options: PropTypes.any, + selectedOptions: PropTypes.any, + setSelectedOptions: PropTypes.func +}; + +export default MultiSelectText; diff --git a/client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.jsx b/client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.jsx new file mode 100644 index 00000000..843c0f76 --- /dev/null +++ b/client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.jsx @@ -0,0 +1,209 @@ +/* eslint-disable no-unused-vars */ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import "react-datepicker/dist/react-datepicker.css"; +import { MdFilterAlt, MdOutlineFilterAlt } from "react-icons/md"; +import { createUseStyles } from "react-jss"; +import { Popover } from "react-tiny-popover"; +import DatePopup from "./DatePopup"; +import TextPopup from "./TextPopup"; +import VisibilityPopup from "./VisibilityPopup"; +import StatusPopup from "./StatusPopup"; +import { useTheme } from "react-jss"; + +const ColumnHeader = React.forwardRef((props, ref) => { + const theme = useTheme(); + + const useStyles = createUseStyles({ + iconNoFilter: { + color: theme.colorWhite, + "&:hover": { + backgroundColor: theme.colorWhite, + color: theme.colorLADOT, + borderRadius: "12.5%", + cursor: "pointer" + } + }, + iconFilter: { + backgroundColor: "transparent", + color: "theme.colorWhite", + marginLeft: "0.5rem" + } + }); + + const classes = useStyles(); + + return ( +
+ {props.header.label} + {props.isFilterApplied() ? ( + + ) : ( + + )} +
+ ); +}); + +ColumnHeader.displayName = "ColumnHeader"; + +ColumnHeader.propTypes = { + onClick: PropTypes.func, + header: PropTypes.any, + isFilterApplied: PropTypes.bool +}; + +const ProjectTableColumnHeader = ({ + projects, + filter, + header, + criteria, + setCriteria, + order, + orderBy, + setSort, + setCheckedProjectIds, + setSelectAllChecked +}) => { + const theme = useTheme(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + // Filter is considered Applied if it is not set + // to the default criteria values. + const isFilterApplied = () => { + let propertyName = header.id; + if (header.popupType === "text") { + propertyName += "List"; + return criteria[propertyName].length > 0; + } + if (header.popupType === "datetime") { + return ( + criteria[header.startDatePropertyName] || + criteria[header.endDatePropertyName] + ); + } + if (header.id === "dateHidden") { + return criteria.visibility !== "visible"; + } + if (header.id === "dateSnapshotted") { + return criteria.type !== "all" || criteria.status !== "active"; + } + return false; + }; + + const handlePopoverToggle = flag => { + setIsPopoverOpen(flag); + }; + + return ( +
+ {header.id !== "checkAllProjects" && header.id !== "contextMenu" ? ( + handlePopoverToggle(false)} + content={ +
+ {!header.popupType ? null : header.popupType === "datetime" ? ( + handlePopoverToggle(false)} + header={header} + criteria={criteria} + setCriteria={setCriteria} + order={order} + orderBy={orderBy} + setSort={setSort} + setCheckedProjectIds={setCheckedProjectIds} + setSelectAllChecked={setSelectAllChecked} + /> + ) : header.popupType === "text" ? ( + handlePopoverToggle(false)} + header={header} + criteria={criteria} + setCriteria={setCriteria} + order={order} + orderBy={orderBy} + setSort={setSort} + setCheckedProjectIds={setCheckedProjectIds} + setSelectAllChecked={setSelectAllChecked} + projects={projects} + filter={filter} + /> + ) : header.popupType === "visibility" ? ( + handlePopoverToggle(false)} + header={header} + criteria={criteria} + setCriteria={setCriteria} + order={order} + orderBy={orderBy} + setSort={setSort} + setCheckedProjectIds={setCheckedProjectIds} + setSelectAllChecked={setSelectAllChecked} + /> + ) : header.popupType === "status" ? ( + handlePopoverToggle(false)} + header={header} + criteria={criteria} + setCriteria={setCriteria} + order={order} + orderBy={orderBy} + setSort={setSort} + setCheckedProjectIds={setCheckedProjectIds} + setSelectAllChecked={setSelectAllChecked} + /> + ) : null} +
+ } + > + setIsPopoverOpen(!isPopoverOpen)} + header={header} + isFilterApplied={() => isFilterApplied()} + > +
+ ) : ( + {header.label} + )} +
+ ); +}; + +ProjectTableColumnHeader.propTypes = { + projects: PropTypes.any, + filter: PropTypes.func, + header: PropTypes.any, + criteria: PropTypes.any, + setCriteria: PropTypes.func, + order: PropTypes.string, + orderBy: PropTypes.string, + setSort: PropTypes.func, + + setCheckedProjectIds: PropTypes.func, + setSelectAllChecked: PropTypes.func +}; + +export default ProjectTableColumnHeader; diff --git a/client/src/components/Projects/ColumnHeaderPopups/StatusPopup.jsx b/client/src/components/Projects/ColumnHeaderPopups/StatusPopup.jsx new file mode 100644 index 00000000..ae34748d --- /dev/null +++ b/client/src/components/Projects/ColumnHeaderPopups/StatusPopup.jsx @@ -0,0 +1,149 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import Button from "../../Button/Button"; +import RadioButton from "../../UI/RadioButton"; +import "react-datepicker/dist/react-datepicker.css"; +import { MdClose } from "react-icons/md"; + +const StatusPopup = ({ + close, + header, + criteria, + setCriteria, + order, + orderBy, + setSort, + setCheckedProjectIds, + setSelectAllChecked +}) => { + const [newOrder, setNewOrder] = useState( + header.id !== orderBy ? null : order + ); + + const [typeSetting, setTypeSetting] = useState(criteria.type); + const [showDeleted, setShowDeleted] = useState(criteria.status === "all"); + + // TODO More state variables for status filtering go here + + const setDefault = () => { + setTypeSetting("all"); + setShowDeleted(false); + setCriteria({ + ...criteria, + status: showDeleted ? "all" : "active", + type: "all" + }); + setCheckedProjectIds([]); + setSelectAllChecked(false); + }; + + const applyChanges = () => { + // Set Criteria for status + setCriteria({ + ...criteria, + status: showDeleted ? "all" : "active", + type: typeSetting + }); + if (newOrder) { + setSort(header.id, newOrder); + } + setCheckedProjectIds([]); + setSelectAllChecked(false); + close(); + }; + + return ( +
+
+ +
+
+ {/* If there is a dateSnapshotted (i.e., project is snapshot), property value is 1 */} + setNewOrder("asc")} + /> + setNewOrder("desc")} + /> +
+
+
+ {/* If there is a dateSnapshotted (i.e., project is snapshot), property value is 1 */} + setTypeSetting("draft")} + /> + setTypeSetting("snapshot")} + /> + setTypeSetting("all")} + /> +
+
+
+ +
+
+
+ + +
+
+ ); +}; + +StatusPopup.propTypes = { + close: PropTypes.func, + header: PropTypes.any, + criteria: PropTypes.any, + setCriteria: PropTypes.func, + order: PropTypes.string, + orderBy: PropTypes.string, + setSort: PropTypes.func, + setCheckedProjectIds: PropTypes.func, + setSelectAllChecked: PropTypes.func +}; + +export default StatusPopup; diff --git a/client/src/components/Projects/ColumnHeaderPopups/TextPopup.jsx b/client/src/components/Projects/ColumnHeaderPopups/TextPopup.jsx new file mode 100644 index 00000000..0d2c753d --- /dev/null +++ b/client/src/components/Projects/ColumnHeaderPopups/TextPopup.jsx @@ -0,0 +1,245 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import Button from "../../Button/Button"; +import RadioButton from "../../UI/RadioButton"; +import "react-datepicker/dist/react-datepicker.css"; +import { MdClose } from "react-icons/md"; +import { MdOutlineSearch } from "react-icons/md"; +import { createUseStyles } from "react-jss"; + +const useStyles = createUseStyles({ + searchBarWrapper: { + width: "100%", + position: "relative", + alignSelf: "center", + marginBottom: "0.5rem" + }, + searchBar: { + maxWidth: "100%", + width: "100%", + padding: "12px 12px 12px 12px", + boxSizing: "border-box" + // marginRight: "0.5rem" + }, + searchIcon: { + position: "absolute", + right: "16px", + top: "14px" + }, + listItem: { + display: "flex", + flexDirection: "row", + alignItems: "center", + height: "2rem", + "&:hover": { + backgroundColor: "lightblue" + } + }, + toggleButton: { + marginRight: "0", + marginTop: "4px", + marginBottom: "4px", + backgroundColor: "transparent", + border: "0", + cursor: "pointer", + textDecoration: "underline", + display: "flex", + fontWeight: "normal" + } +}); + +const TextPopup = ({ + projects, + filter, + close, + header, + criteria, + setCriteria, + order, + orderBy, + setSort, + setCheckedProjectIds, + setSelectAllChecked +}) => { + const classes = useStyles(); + + const [newOrder, setNewOrder] = useState( + header.id !== orderBy ? null : order + ); + const [selectedListItems, setSelectedListItems] = useState( + criteria[header.id + "List"].map(s => ({ value: s, label: s })) + ); + const [searchString, setSearchString] = useState(""); + + const initiallyChecked = o => criteria[header.id + "List"].includes(o); + + // To build the drop-down list, we want to apply all the criteria that + // are currently selected EXCEPT the criteria we are currently editing. + const listCriteria = { ...criteria, [header.id + "List"]: [] }; + const filteredProjects = projects.filter(p => filter(p, listCriteria)); + const property = header.id == "author" ? "fullname" : header.id; + const selectOptions = [...new Set(filteredProjects.map(p => p[property]))] + .filter(value => value !== null) + .sort( + (a, b) => (initiallyChecked(b) ? 1 : 0) - (initiallyChecked(a) ? 1 : 0) + ); + + const filteredOptions = selectOptions + .filter(o => !!o) + .filter(opt => opt.toLowerCase().includes(searchString.toLowerCase())); + + const onChangeSearchString = e => { + setSearchString(e.target.value); + }; + + const handleCheckboxChange = e => { + const optionValue = e.target.name; + if (!e.target.checked) { + const newSelectedListItems = selectedListItems.filter( + selectedOption => selectedOption.value !== optionValue + ); + setSelectedListItems(newSelectedListItems); + } else { + const newSelectedListItems = [ + ...selectedListItems, + { value: optionValue, label: optionValue } + ]; + setSelectedListItems(newSelectedListItems); + } + }; + + const isChecked = optionValue => { + const checked = selectedListItems.find( + option => option.value == optionValue + ); + return !!checked; + }; + + const applyChanges = () => { + setCriteria({ + ...criteria, + [header.id + "List"]: selectedListItems.map(sli => sli.value) + }); + if (newOrder) { + setSort(header.id, newOrder); + } + setCheckedProjectIds([]); + setSelectAllChecked(false); + close(); + }; + + const setDefault = () => { + setNewOrder(null); + setSelectedListItems([]); + setCheckedProjectIds([]); + setSelectAllChecked(false); + }; + + return ( +
+
+ +
+
+ setNewOrder("asc")} + /> + setNewOrder("desc")} + /> +
+
+ +
+ +
{`${selectedListItems.length} selected`}
+
+
+ + +
+ +
+ {/*
{JSON.stringify(selectedListItems, null, 2)}
*/} + {/*
{JSON.stringify(options, null, 2)}
*/} + + {filteredOptions.map(o => ( +
+ + {o} +
+ ))} +
+ +
+
+ + +
+
+ ); +}; + +TextPopup.propTypes = { + projects: PropTypes.any, + filter: PropTypes.func, + close: PropTypes.func, + header: PropTypes.any, + criteria: PropTypes.any, + setCriteria: PropTypes.func, + order: PropTypes.string, + orderBy: PropTypes.string, + setSort: PropTypes.func, + setCheckedProjectIds: PropTypes.func, + setSelectAllChecked: PropTypes.func +}; + +export default TextPopup; diff --git a/client/src/components/Projects/ColumnHeaderPopups/VisibilityPopup.jsx b/client/src/components/Projects/ColumnHeaderPopups/VisibilityPopup.jsx new file mode 100644 index 00000000..98a3d015 --- /dev/null +++ b/client/src/components/Projects/ColumnHeaderPopups/VisibilityPopup.jsx @@ -0,0 +1,132 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import Button from "../../Button/Button"; +import RadioButton from "../../UI/RadioButton"; +import "react-datepicker/dist/react-datepicker.css"; +import { MdClose } from "react-icons/md"; + +const VisibilityPopup = ({ + close, + header, + criteria, + setCriteria, + order, + orderBy, + setSort, + setCheckedProjectIds, + setSelectAllChecked +}) => { + const [newOrder, setNewOrder] = useState( + header.id !== orderBy ? null : order + ); + + const [visibilitySetting, setVisibilitySetting] = useState( + criteria.visibility + ); + + const setDefault = () => { + setVisibilitySetting("visible"); + setCriteria({ + ...criteria, + visibility: "visible" + }); + setCheckedProjectIds([]); + setSelectAllChecked(false); + }; + + const applyChanges = () => { + setCriteria({ + ...criteria, + visibility: visibilitySetting + }); + if (newOrder) { + setSort(header.id, newOrder); + } + setCheckedProjectIds([]); + setSelectAllChecked(false); + close(); + }; + + return ( +
+
+ +
+
+ {/* If there is a dateHidden, property value is 1 */} + setNewOrder("asc")} + /> + setNewOrder("desc")} + /> +
+
+
+ {/* If there is a dateSnapshotted (i.e., project is snapshot), property value is 1 */} + setVisibilitySetting("visible")} + /> + setVisibilitySetting("hidden")} + /> + setVisibilitySetting("all")} + /> +
+ +
+
+ + +
+
+ ); +}; + +VisibilityPopup.propTypes = { + close: PropTypes.func, + header: PropTypes.any, + criteria: PropTypes.any, + setCriteria: PropTypes.func, + order: PropTypes.string, + orderBy: PropTypes.string, + setSort: PropTypes.func, + setCheckedProjectIds: PropTypes.func, + setSelectAllChecked: PropTypes.func +}; + +export default VisibilityPopup; diff --git a/client/src/components/Projects/CopyProjectModal.js b/client/src/components/Projects/CopyProjectModal.jsx similarity index 97% rename from client/src/components/Projects/CopyProjectModal.js rename to client/src/components/Projects/CopyProjectModal.jsx index 2bbc4544..1981f7ad 100644 --- a/client/src/components/Projects/CopyProjectModal.js +++ b/client/src/components/Projects/CopyProjectModal.jsx @@ -56,7 +56,7 @@ export default function CopyProjectModal({ />
-
- Warning + Are you sure you want to restore the project from the trash,
@@ -61,11 +57,7 @@ const DeleteProjectModal = ({ mounted, onClose, project }) => { Delete Project
- Warning + Are you sure you want to delete the following?

(It will remain in the recycling bin for ninety days

before being permanently deleted) @@ -76,7 +68,7 @@ const DeleteProjectModal = ({ mounted, onClose, project }) => { {Array.isArray(project.name) ? project.name.join(", ") : project.name}
- {project.dateTrashed ? ( diff --git a/client/src/components/Projects/DownloadProjectModal.js b/client/src/components/Projects/DownloadProjectModal.jsx similarity index 92% rename from client/src/components/Projects/DownloadProjectModal.js rename to client/src/components/Projects/DownloadProjectModal.jsx index f4674b7b..e0bf769c 100644 --- a/client/src/components/Projects/DownloadProjectModal.js +++ b/client/src/components/Projects/DownloadProjectModal.jsx @@ -1,14 +1,13 @@ import React, { useState, useEffect } from "react"; import PropTypes from "prop-types"; import { createUseStyles, useTheme } from "react-jss"; -import * as projectResultService from "..////../services/projectResult.service"; - +import * as projectResultService from "../../services/projectResult.service"; import Button from "../Button/Button"; -// import { faCopy } from "@fortawesome/free-solid-svg-icons"; -// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import ModalDialog from "../UI/AriaModal/ModalDialog"; +// This file appears to be obsolete + const useStyles = createUseStyles(theme => ({ buttonFlexBox: { display: "flex", @@ -96,7 +95,7 @@ export default function DownloadProjectModal({
- {/*
- + +

Status

- + options={statusOptions} + onChange={handleChange} + name="status" + className={classes.dropdown} + />

Visibility

- + options={visibilityOptions} + onChange={handleChange} + name="visibility" + className={classes.dropdown} + />

Project Name

handleChange(e, "name")} className={classes.textInput} @@ -135,15 +165,17 @@ const FilterPopup = ({

Address

handleChange(e, "address")} className={classes.textInput} /> - {account.isAdmin && ( + {account?.isAdmin && ( <>

Author

handleChange(e, "author")} className={classes.textInput} @@ -154,6 +186,7 @@ const FilterPopup = ({

Alternative Number

handleChange(e, "alternative")} className={classes.textInput} diff --git a/client/src/components/Projects/MultiProjectToolbarMenu.js b/client/src/components/Projects/MultiProjectToolbarMenu.jsx similarity index 89% rename from client/src/components/Projects/MultiProjectToolbarMenu.js rename to client/src/components/Projects/MultiProjectToolbarMenu.jsx index 3c6fc845..f107e693 100644 --- a/client/src/components/Projects/MultiProjectToolbarMenu.js +++ b/client/src/components/Projects/MultiProjectToolbarMenu.jsx @@ -2,7 +2,7 @@ import React, { useContext, useRef } from "react"; import UserContext from "../../contexts/UserContext"; import PropTypes from "prop-types"; import { createUseStyles } from "react-jss"; -import { FaFileCsv } from "react-icons/fa"; +import { FaFileCsv } from "react-icons/fa6"; import { MdDelete, MdRestoreFromTrash, @@ -13,6 +13,9 @@ import { import { Tooltip } from "react-tooltip"; import PdfPrint from "../PdfPrint/PdfPrint"; import { useReactToPrint } from "react-to-print"; +import { ENABLE_UPDATE_TOTALS } from "../../helpers/Constants"; + +import * as projectResultService from "../../services/projectResult.service"; const useStyles = createUseStyles({ container: { @@ -60,7 +63,7 @@ const MultiProjectToolbarMenu = ({ ) { project = checkedProjectsStatusData; } - const isProjectOwner = account.id === project?.loginId; + const isProjectOwner = account ? account.id === project?.loginId : false; const isBtnDisabled = (projProp, criteriaProp) => { const sameDateVals = checkedProjectsStatusData[projProp] !== false; @@ -105,12 +108,29 @@ const MultiProjectToolbarMenu = ({ pageStyle: ".printContainer {overflow: hidden;}" }); + const handleComputeTotals = async () => { + for (let i = 0; i < checkedProjectIds.length; i++) { + await projectResultService.populateTargetPoints(checkedProjectIds[i]); + } + }; + return (
{checkedProjectIds.length} Projects Selected
    + {ENABLE_UPDATE_TOTALS ? ( +
  • + +
  • + ) : null}
  • - } - position="left center" - offsetX={-10} - on="hover" - arrow={true} - > - {close => ( - handleCsvModalOpen(ev, project)} - handleCopyModalOpen={handleCopyModalOpen} - handleDeleteModalOpen={handleDeleteModalOpen} - handlePrintPdf={handlePrintPdf} - handleSnapshotModalOpen={handleSnapshotModalOpen} - handleRenameSnapshotModalOpen={handleRenameSnapshotModalOpen} - handleHide={handleHide} - /> - )} - -
    - -
    -
- )} - - - ); -}; - -ProjectTableRow.propTypes = { - project: PropTypes.object.isRequired, - handleCsvModalOpen: PropTypes.func.isRequired, - handleCopyModalOpen: PropTypes.func.isRequired, - handleDeleteModalOpen: PropTypes.func.isRequired, - handleSnapshotModalOpen: PropTypes.func.isRequired, - handleRenameSnapshotModalOpen: PropTypes.func.isRequired, - handleHide: PropTypes.func.isRequired, - handleCheckboxChange: PropTypes.func.isRequired, - checkedProjectIds: PropTypes.arrayOf(PropTypes.number).isRequired -}; - -export default ProjectTableRow; diff --git a/client/src/components/Projects/ProjectTableRow.jsx b/client/src/components/Projects/ProjectTableRow.jsx new file mode 100644 index 00000000..b16aaccd --- /dev/null +++ b/client/src/components/Projects/ProjectTableRow.jsx @@ -0,0 +1,387 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { createUseStyles } from "react-jss"; +import { useState, useRef, useEffect } from "react"; +import PropTypes from "prop-types"; +import Popup from "reactjs-popup"; +import "reactjs-popup/dist/index.css"; +import { + MdVisibility, + MdVisibilityOff, + MdMoreVert, + MdEdit, + MdAddCircle +} from "react-icons/md"; +import { formatDate } from "../../helpers/util"; +import { useReactToPrint } from "react-to-print"; +import ProjectContextMenu from "./ProjectContextMenu"; +import PdfPrint from "../PdfPrint/PdfPrint"; +import fetchEngineRules from "./fetchEngineRules"; +import * as droService from "../../services/dro.service"; +import UniversalSelect from "../UI/UniversalSelect"; +import { ENABLE_UPDATE_TOTALS } from "../../helpers/Constants"; + +const useStyles = createUseStyles({ + td: { + padding: "0.2em", + textAlign: "left" + }, + tdRightAlign: { + padding: "0.2em", + textAlign: "right" + }, + tdCenterAlign: { + padding: "0.2em", + textAlign: "center" + }, + actionIcons: { + display: "flex", + justifyContent: "space-around", + width: "auto", + "& button": { + border: "none", + backgroundColor: "transparent", + "&:hover": { + cursor: "pointer" + } + } + }, + + selectBox: { + padding: "0.5em", + border: "1px solid #ccc", + borderRadius: "4px", + fontSize: "14px" + }, + adminNoteInput: { + padding: "0.3em", + marginRight: "0.5em", + flexGrow: 1 + }, + saveButton: { + padding: "0.3em 0.6em", + marginRight: "0.3em", + backgroundColor: "#4CAF50", + color: "white", + border: "none", + borderRadius: "4px", + cursor: "pointer", + "&:disabled": { + backgroundColor: "#a5d6a7", + cursor: "not-allowed" + } + }, + cancelButton: { + padding: "0.3em 0.6em", + backgroundColor: "#f44336", + color: "white", + border: "none", + borderRadius: "4px", + cursor: "pointer" + } +}); + +const ProjectTableRow = ({ + project, + handleCsvModalOpen, + handleCopyModalOpen, + handleDeleteModalOpen, + handleSnapshotModalOpen, + handleRenameSnapshotModalOpen, + handleShareSnapshotModalOpen, + handleHide, + handleCheckboxChange, + checkedProjectIds, + isAdmin, + droOptions, + onDroChange, // New prop + onAdminNoteUpdate // New prop +}) => { + const classes = useStyles(); + const formInputs = JSON.parse(project.formInputs); + const printRef = useRef(); + const [projectRules, setProjectRules] = useState(null); + const [selectedDro, setSelectedDro] = useState(project.droId || ""); + const [droName, setDroName] = useState("N/A"); + const [editingNote, setEditingNote] = useState(false); + const [adminNote, setAdminNote] = useState(project.adminNotes || ""); + const [tempAdminNote, setTempAdminNote] = useState(project.adminNotes || ""); + // Download and process rules for PDF rendering + useEffect(() => { + const fetchRules = async () => { + const result = await fetchEngineRules(project); + setProjectRules(result); + }; + + fetchRules() + // TODO: do we have better reporting than this? + .catch(console.error); + }, [project]); + + useEffect(() => { + if (!isAdmin && project.droId) { + const fetchDroById = async () => { + try { + const response = await droService.getById(project.droId); + setDroName(response.data.name || "N/A"); + } catch (error) { + console.error("Error fetching DRO by ID", error); + setDroName("N/A"); + } + }; + fetchDroById(); + } + }, [isAdmin, project.droId]); + + const handleSave = () => { + if (tempAdminNote.trim() !== adminNote.trim()) { + onAdminNoteUpdate(project.id, tempAdminNote.trim()); + } + setEditingNote(false); + }; + + const handleCancel = () => { + setTempAdminNote(adminNote || ""); + setEditingNote(false); + }; + + useEffect(() => { + setAdminNote(project.adminNotes || ""); + }, [project.adminNotes]); + + useEffect(() => { + setSelectedDro(project.droId || ""); + }, [project.droId]); + + useEffect(() => { + setTempAdminNote(project.adminNotes || ""); + }, [project.adminNotes]); + + const handlePrintPdf = useReactToPrint({ + content: () => printRef.current, + bodyClass: "printContainer", + pageStyle: ".printContainer {overflow: hidden;}" + }); + + const fallbackToBlank = value => { + return value !== "undefined" ? value : ""; + }; + + const dateSubmittedDisplay = () => { + if (project.dateSubmitted) { + return {formatDate(project.dateSubmitted)}; + } + return {formatDate(project.dateSubmitted)}; + }; + + return ( + + + handleCheckboxChange(project.id)} + /> + + + {project.dateHidden ? ( + + ) : ( + + )} + + + {project.dateSnapshotted ? "Snapshot" : "Draft"} + {project.dateTrashed ? (deleted) : null} + + + {project.name} + + {project.address} + {fallbackToBlank(formInputs.VERSION_NO)} + + {`${project.firstName} ${project.lastName}`} + + + {formatDate(project.dateCreated)} + + + {formatDate(project.dateModified)} + + {dateSubmittedDisplay()} + {/* DRO Column */} + + {isAdmin && droOptions.data ? ( +
+ { + const newDroId = e.target.value; + setSelectedDro(newDroId); + onDroChange(project.id, newDroId); + }} + options={[ + { value: "", label: "Select..." }, + ...droOptions.data.map(dro => ({ + value: dro.id, + label: dro.name + })) + ]} + name="droId" + className={classes.selectBox} + /> +
+ ) : ( + {droName} + )} + + {isAdmin && ( + +
+ {editingNote ? ( +
+ setTempAdminNote(e.target.value)} + autoFocus + className={classes.adminNoteInput} + /> + + +
+ ) : ( +
+ {adminNote && {adminNote}} + +
+ )} +
+ + )} + {isAdmin && ( + + + {project.dateModifiedAdmin + ? formatDate(project.dateModifiedAdmin) + : "N/A"} + + + )} + + {projectRules && ( +
+ + + + } + position="left center" + offsetX={-10} + on="hover" + arrow={true} + > + {close => ( + handleCsvModalOpen(ev, project)} + handleCopyModalOpen={handleCopyModalOpen} + handleDeleteModalOpen={handleDeleteModalOpen} + handlePrintPdf={handlePrintPdf} + handleSnapshotModalOpen={handleSnapshotModalOpen} + handleRenameSnapshotModalOpen={handleRenameSnapshotModalOpen} + handleShareSnapshotModalOpen={handleShareSnapshotModalOpen} + handleHide={handleHide} + /> + )} + +
+ +
+
+ )} + {" "} + {ENABLE_UPDATE_TOTALS ? ( + + {`${project.targetPoints}/${project.earnedPoints}/${project.projectLevel}`} + + ) : null} + + ); +}; + +ProjectTableRow.propTypes = { + project: PropTypes.any, + handleCsvModalOpen: PropTypes.func.isRequired, + handleCopyModalOpen: PropTypes.func.isRequired, + handleDeleteModalOpen: PropTypes.func.isRequired, + handleSnapshotModalOpen: PropTypes.func.isRequired, + handleRenameSnapshotModalOpen: PropTypes.func.isRequired, + handleShareSnapshotModalOpen: PropTypes.func.isRequired, + handleHide: PropTypes.func.isRequired, + handleCheckboxChange: PropTypes.func.isRequired, + checkedProjectIds: PropTypes.arrayOf(PropTypes.number).isRequired, + isAdmin: PropTypes.bool.isRequired, + droOptions: PropTypes.shape({ + data: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired + }) + ).isRequired + }).isRequired, + onDroChange: PropTypes.func.isRequired, // New propType + onAdminNoteUpdate: PropTypes.func.isRequired // New propType +}; + +export default ProjectTableRow; diff --git a/client/src/components/Projects/ProjectTableRow.test.js b/client/src/components/Projects/ProjectTableRow.test.jsx similarity index 55% rename from client/src/components/Projects/ProjectTableRow.test.js rename to client/src/components/Projects/ProjectTableRow.test.jsx index 08ddfe90..36dfac58 100644 --- a/client/src/components/Projects/ProjectTableRow.test.js +++ b/client/src/components/Projects/ProjectTableRow.test.jsx @@ -3,6 +3,7 @@ import { BrowserRouter } from "react-router-dom"; import ProjectTableRow from "./ProjectTableRow"; import { render, screen, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; +import TdmAuthProvider from "../Layout/TdmAuthProvider"; jest.mock("react-to-print", () => ({ __esModule: true, @@ -10,7 +11,6 @@ jest.mock("react-to-print", () => ({ useReactToPrint: jest.fn() })); -// Set up a Jest mock function to check any props. const mockPrint = jest.fn(); jest.mock("../PdfPrint/PdfPrint", () => { const { forwardRef } = jest.requireActual("react"); @@ -22,7 +22,7 @@ jest.mock("../PdfPrint/PdfPrint", () => { }) }; }); -// Set up a Jest mock function to check any props. + const mockCsv = jest.fn(); jest.mock("react-csv", () => { const { forwardRef } = jest.requireActual("react"); @@ -80,10 +80,23 @@ describe("ProjectTableRow", () => { formInputs: "{}" }; + const handleCsvModalOpen = jest.fn(); const handleCopyModalOpen = jest.fn(); const handleDeleteModalOpen = jest.fn(); const handleSnapshotModalOpen = jest.fn(); + const handleRenameSnapshotModalOpen = jest.fn(); const handleHide = jest.fn(); + const handleCheckboxChange = jest.fn(); + const checkedProjectIds = []; + const isAdmin = false; + const droOptions = { + data: [ + { id: 1, name: "DRO #1" }, + { id: 2, name: "DRO #2" } + ] + }; + const onDroChange = jest.fn(); + const onAdminNoteUpdate = jest.fn(); beforeEach(() => { jest.clearAllMocks(); @@ -94,16 +107,27 @@ describe("ProjectTableRow", () => { // in order to generate the CSV and PDF. // and the tests need a chance to process the setProjectData. it("renders project data", async () => { + const fetchEngineRules = require("./fetchEngineRules"); + fetchEngineRules.mockImplementation(() => Promise.resolve(projectRules)); + render(
@@ -122,12 +146,18 @@ describe("ProjectTableRow", () => { expect(screen.queryByTitle("Snapshot")).not.toBeInTheDocument(); expect(screen.getByText("Draft")).toBeInTheDocument(); - await waitFor(() => expect(screen.getByRole("button")).toBeInTheDocument()); + await waitFor(() => + expect( + screen.getByRole("button", { name: "context menu button" }) + ).toBeInTheDocument() + ); }); it("renders project form inputs", async () => { const formInputs = { VERSION_NO: "98.6", BUILDING_PERMIT: "12345" }; project.formInputs = JSON.stringify(formInputs); + const fetchEngineRules = require("./fetchEngineRules"); + fetchEngineRules.mockImplementation(() => Promise.resolve(projectRules)); render( @@ -135,10 +165,18 @@ describe("ProjectTableRow", () => { @@ -150,31 +188,51 @@ describe("ProjectTableRow", () => { screen.queryByText(formInputs.BUILDING_PERMIT) ).not.toBeInTheDocument(); - await waitFor(() => expect(screen.getByRole("button")).toBeInTheDocument()); + await waitFor(() => + expect( + screen.getByRole("button", { name: "context menu button" }) + ).toBeInTheDocument() + ); }); it("renders project date modified", async () => { const dateModified = "2023-10-02T12:34:56.789Z"; project.dateModified = dateModified; + const fetchEngineRules = require("./fetchEngineRules"); + fetchEngineRules.mockImplementation(() => Promise.resolve(projectRules)); render( - - - - -
+ + + + + +
+
); expect(screen.getByText("2023-10-02")).toBeInTheDocument(); - await waitFor(() => expect(screen.getByRole("button")).toBeInTheDocument()); + await waitFor(() => + expect( + screen.getByRole("button", { name: "context menu button" }) + ).toBeInTheDocument() + ); }); it("renders project hidden/trashed/snapshotted icons", async () => { @@ -184,6 +242,8 @@ describe("ProjectTableRow", () => { project.dateHidden = dateHidden; project.dateTrashed = dateTrashed; project.dateSnapshotted = dateSnapshotted; + const fetchEngineRules = require("./fetchEngineRules"); + fetchEngineRules.mockImplementation(() => Promise.resolve(projectRules)); render( @@ -191,24 +251,38 @@ describe("ProjectTableRow", () => { ); - expect(screen.queryByRole("button")).toBeNull(); + expect( + screen.queryByRole("button", { name: "context menu button" }) + ).toBeNull(); expect(screen.getByTitle("Project is hidden")).toBeInTheDocument(); - expect(screen.getByText("-Deleted")).toBeInTheDocument(); + expect(screen.getByText("(deleted)")).toBeInTheDocument(); expect(screen.queryByTitle("Draft")).not.toBeInTheDocument(); expect(screen.getByText("Snapshot")).toBeInTheDocument(); - await waitFor(() => expect(screen.getByRole("button")).toBeInTheDocument()); + await waitFor(() => + expect( + screen.getByRole("button", { name: "context menu button" }) + ).toBeInTheDocument() + ); }); it("renders async project context menu", async () => { @@ -220,10 +294,18 @@ describe("ProjectTableRow", () => { @@ -232,32 +314,36 @@ describe("ProjectTableRow", () => { expect(fetchEngineRules).toHaveBeenCalledTimes(1); expect(screen.queryByRole("button")).toBeNull(); - await waitFor(() => expect(screen.getByRole("button")).toBeInTheDocument()); + await waitFor(() => + expect( + screen.getByRole("button", { name: "context menu button" }) + ).toBeInTheDocument() + ); expect(mockPrint).toHaveBeenCalledTimes(1); expect(mockPrint).toHaveBeenCalledWith( expect.objectContaining({ - dateModified: "10/02/2023", + project: project, rules: projectRules }) ); - expect(mockCsv).toHaveBeenCalledTimes(1); - expect(mockCsv).toHaveBeenCalledWith( - expect.objectContaining({ - filename: "TDM-data.csv", - target: "_blank", - data: expect.arrayContaining([ - expect.arrayContaining(["TDM Calculation Project Summary"]), - expect.arrayContaining([ - "Rule 1", - "Rule 2", - "Rule 3 - Choice 1", - "Rule 3 - Choice 2", - "Rule 3 - Choice 3" - ]), - expect.arrayContaining(["1", "2", "Y", "N", "N"]) - ]) - }) - ); + // expect(mockCsv).toHaveBeenCalledTimes(1); + // expect(mockCsv).toHaveBeenCalledWith( + // expect.objectContaining({ + // filename: "TDM-data.csv", + // target: "_blank", + // data: expect.arrayContaining([ + // expect.arrayContaining(["TDM Calculation Project Summary"]), + // expect.arrayContaining([ + // "Rule 1", + // "Rule 2", + // "Rule 3 - Choice 1", + // "Rule 3 - Choice 2", + // "Rule 3 - Choice 3" + // ]), + // expect.arrayContaining(["1", "2", "Y", "N", "N"]) + // ]) + // }) + // ); }); }); diff --git a/client/src/components/Projects/ProjectsPage.js b/client/src/components/Projects/ProjectsPage.jsx similarity index 64% rename from client/src/components/Projects/ProjectsPage.js rename to client/src/components/Projects/ProjectsPage.jsx index b369f297..c1f301ad 100644 --- a/client/src/components/Projects/ProjectsPage.js +++ b/client/src/components/Projects/ProjectsPage.jsx @@ -2,25 +2,29 @@ import React, { useState, useContext, useEffect, memo } from "react"; import PropTypes from "prop-types"; import { useNavigate } from "react-router-dom"; import { createUseStyles } from "react-jss"; -import UserContext from "../../contexts/UserContext.js"; -import { MdFilterAlt, MdArrowDropDown, MdArrowDropUp } from "react-icons/md"; -import SearchIcon from "../../images/search.png"; -import Pagination from "../UI/Pagination.js"; +import UserContext from "../../contexts/UserContext"; +import { MdOutlineSearch } from "react-icons/md"; +import Pagination from "../UI/Pagination"; import ContentContainerNoSidebar from "../Layout/ContentContainerNoSidebar"; import useErrorHandler from "../../hooks/useErrorHandler"; import useProjects from "../../hooks/useGetProjects"; -import useCheckedProjectsStatusData from "../../hooks/useCheckedProjectsStatusData.js"; +import useCheckedProjectsStatusData from "../../hooks/useCheckedProjectsStatusData"; import * as projectService from "../../services/project.service"; +import * as projectResultService from "../../services/projectResult.service"; +import * as droService from "../../services/dro.service"; import SnapshotProjectModal from "./SnapshotProjectModal"; import RenameSnapshotModal from "./RenameSnapshotModal"; +import ShareSnapshotModal from "./ShareSnapshotModal"; import DeleteProjectModal from "./DeleteProjectModal"; import CopyProjectModal from "./CopyProjectModal"; -import CsvModal from "./CsvModal.js"; +import CsvModal from "./CsvModal"; import ProjectTableRow from "./ProjectTableRow"; -import FilterDrawer from "./FilterDrawer.js"; -import MultiProjectToolbarMenu from "./MultiProjectToolbarMenu.js"; -import fetchEngineRules from "./fetchEngineRules.js"; +// import FilterDrawer from "./FilterDrawer"; +import MultiProjectToolbarMenu from "./MultiProjectToolbarMenu"; +import fetchEngineRules from "./fetchEngineRules"; +import UniversalSelect from "../UI/UniversalSelect"; +import ProjectTableColumnHeader from "./ColumnHeaderPopups/ProjectTableColumnHeader"; const useStyles = createUseStyles({ outerDiv: { @@ -59,7 +63,7 @@ const useStyles = createUseStyles({ }, searchBar: { maxWidth: "100%", - width: "20em", + width: "27em", padding: "12px 12px 12px 48px", marginRight: "0.5rem" }, @@ -114,7 +118,7 @@ const useStyles = createUseStyles({ textAlign: "center" }, tableContainer: { - overflow: "auto", + overflow: "visible", // changed to allow Universal Select to show above the page container when expanded width: "100%", margin: "20px 0px" }, @@ -125,9 +129,9 @@ const useStyles = createUseStyles({ justifyContent: "center" }, dropContent: { - padding: "5px", - borderColor: "silver", - borderRadius: "4px" + borderRadius: "4px", + width: "60px", + textAlign: "center" }, optionItems: { backgroundColor: "white", @@ -145,24 +149,103 @@ const ProjectsPage = ({ contentContainerRef }) => { const userContext = useContext(UserContext); const [filterText, setFilterText] = useState(""); - const [order, setOrder] = useState("asc"); + const [order, setOrder] = useState("desc"); + const [orderBy, setOrderBy] = useState("dateModified"); const email = userContext.account ? userContext.account.email : ""; const navigate = useNavigate(); const handleError = useErrorHandler(email, navigate); const [projects, setProjects] = useProjects(handleError); - const [orderBy, setOrderBy] = useState("dateCreated"); const [copyModalOpen, setCopyModalOpen] = useState(false); const [snapshotModalOpen, setSnapshotModalOpen] = useState(false); const [renameSnapshotModalOpen, setRenameSnapshotModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [csvModalOpen, setCsvModalOpen] = useState(false); + const [shareSnapshotModalOpen, setShareSnapshotModalOpen] = useState(false); const [selectedProject, setSelectedProject] = useState(null); const [checkedProjectIds, setCheckedProjectIds] = useState([]); const [selectAllChecked, setSelectAllChecked] = useState(false); const [projectData, setProjectData] = useState(); const [currentPage, setCurrentPage] = useState(1); const [perPage, setPerPage] = useState(10); + const [droOptions, setDroOptions] = useState([]); + const [droNameMap, setDroNameMap] = useState({}); const projectsPerPage = perPage; + const isAdmin = userContext.account?.isAdmin || false; + + useEffect(() => { + // Check if the user is an admin + const isAdmin = userContext.account?.isAdmin || false; + + if (isAdmin) { + // Fetch available DROs + + const fetchDroOptions = async () => { + const result = await droService.get(); + setDroOptions(result); + }; + fetchDroOptions().catch(console.error); + } + }, [userContext.account]); + + useEffect(() => { + const isAdmin = userContext.account?.isAdmin || false; + + if (!isAdmin) { + // Extract unique droIds from projects + const uniqueDroIds = [ + ...new Set( + projects + .filter(project => project.droId) + .map(project => project.droId) + ) + ]; + + // Function to fetch DRO names for given droIds + const fetchDroNames = async () => { + try { + // Initialize a temporary map + const tempDroNameMap = {}; + + // Fetch each DRO by ID + await Promise.all( + uniqueDroIds.map(async droId => { + try { + const response = await droService.getById(droId); + tempDroNameMap[droId] = response.data.name || "N/A"; + } catch (error) { + console.error(`Error fetching DRO with ID ${droId}:`, error); + tempDroNameMap[droId] = "N/A"; + } + }) + ); + + // Update the droNameMap state + setDroNameMap(tempDroNameMap); + } catch (error) { + console.error("Error fetching DRO names:", error); + } + }; + + // Only fetch if there are droIds to fetch + if (uniqueDroIds.length > 0) { + fetchDroNames(); + } else { + // Reset the map if no droIds are present + setDroNameMap({}); + } + } else { + // If admin, reset the map since admin already has droOptions + setDroNameMap({}); + } + }, [projects, userContext.account?.isAdmin]); + + const perPageOptions = [ + { value: projects.length, label: "All" }, + { value: 100, label: "100" }, + { value: 50, label: "50" }, + { value: 25, label: "25" }, + { value: 10, label: "10" } + ]; const handlePerPageChange = newPerPage => { setPerPage(newPerPage); @@ -185,12 +268,21 @@ const ProjectsPage = ({ contentContainerRef }) => { address: "", author: "", alternative: "", + dro: "", startDateCreated: null, endDateCreated: null, startDateModified: null, - endDateModified: null + endDateModified: null, + nameList: [], + addressList: [], + alternativeList: [], + authorList: [], + droList: [], + adminNotes: "", + startDateModifiedAdmin: null, + endDateModifiedAdmin: null }); - const [filterCollapsed, setFilterCollapsed] = useState(true); + const checkedProjectsStatusData = useCheckedProjectsStatusData( checkedProjectIds, projects @@ -254,11 +346,16 @@ const ProjectsPage = ({ contentContainerRef }) => { }; const handleCopyModalClose = async (action, newProjectName) => { + let newSelectedProject = { ...selectedProject }; if (action === "ok") { const projectFormInputsAsJson = JSON.parse(selectedProject.formInputs); projectFormInputsAsJson.PROJECT_NAME = newProjectName; + if (!selectedProject.targetPoints) { + await projectResultService.populateTargetPoints(selectedProject); + newSelectedProject = await projectService.getById(selectedProject.id); + } let newProject = { - ...selectedProject, + ...newSelectedProject, name: newProjectName, formInputs: JSON.stringify(projectFormInputsAsJson) }; @@ -355,6 +452,15 @@ const ProjectsPage = ({ contentContainerRef }) => { setRenameSnapshotModalOpen(false); }; + const handleShareSnapshotModalOpen = project => { + setSelectedProject(project); + setShareSnapshotModalOpen(true); + }; + + const handleShareSnapshotModalClose = async () => { + setShareSnapshotModalOpen(false); + }; + const handleHide = async project => { try { if (!checkedProjectIds.length) { @@ -417,7 +523,7 @@ const ProjectsPage = ({ contentContainerRef }) => { setSelectAllChecked(!selectAllChecked); }; - const descCompareBy = (a, b, orderBy) => { + const ascCompareBy = (a, b, orderBy) => { let projectA, projectB; if (orderBy === "VERSION_NO") { @@ -434,6 +540,9 @@ const ProjectsPage = ({ contentContainerRef }) => { projectB = JSON.parse(b.formInputs).BUILDING_PERMIT ? JSON.parse(b.formInputs).BUILDING_PERMIT : "undefined"; + } else if (orderBy === "author") { + projectA = `${a["lastName"]} ${a["firstName"]}`; + projectB = `${b["lastName"]} ${b["firstName"]}`; } else if ( orderBy === "dateHidden" || orderBy === "dateTrashed" || @@ -441,6 +550,13 @@ const ProjectsPage = ({ contentContainerRef }) => { ) { projectA = a[orderBy] ? 1 : 0; projectB = b[orderBy] ? 1 : 0; + } else if ( + orderBy === "dateSubmitted" || + orderBy === "dateCreated" || + orderBy === "dateModified" + ) { + projectA = a[orderBy] ? a[orderBy] : "2000-01-01"; + projectB = b[orderBy] ? b[orderBy] : "2000-01-01"; } else { projectA = a[orderBy].toLowerCase(); projectB = b[orderBy].toLowerCase(); @@ -456,9 +572,9 @@ const ProjectsPage = ({ contentContainerRef }) => { }; const getComparator = (order, orderBy) => { - return order === "desc" - ? (a, b) => descCompareBy(a, b, orderBy) - : (a, b) => -descCompareBy(a, b, orderBy); + return order === "asc" + ? (a, b) => ascCompareBy(a, b, orderBy) + : (a, b) => -ascCompareBy(a, b, orderBy); }; const stableSort = (array, comparator) => { @@ -471,13 +587,28 @@ const ProjectsPage = ({ contentContainerRef }) => { return stabilizedList.map(el => el[0]); }; - const handleSort = property => { - // disable sorting for header checkbox - if (property === "checkAllProjects") return; + const setSort = (orderBy, order) => { + setOrder(order); + setOrderBy(orderBy); + }; - const isAsc = orderBy === property && order === "asc"; - setOrder(isAsc ? "desc" : "asc"); - setOrderBy(property); + const handleDroChange = async (projectId, newDroId) => { + try { + const updatedDroId = newDroId === "" ? null : newDroId; + await projectService.updateDroId(projectId, updatedDroId); + await updateProjects(); // Refresh the project list + } catch (error) { + console.error("Error updating DRO ID:", error); + } + }; + + const handleAdminNoteUpdate = async (projectId, newAdminNote) => { + try { + await projectService.updateAdminNotes(projectId, newAdminNote); + await updateProjects(); // Refresh the project list + } catch (error) { + console.error("Error updating admin notes:", error); + } }; const handleFilterTextChange = text => { @@ -489,7 +620,7 @@ const ProjectsPage = ({ contentContainerRef }) => { return new Date(dateOnly); }; - const filterProjects = p => { + const filter = (p, criteria) => { if (criteria.type === "draft" && p.dateSnapshotted) return false; if (criteria.type === "snapshot" && !p.dateSnapshotted) return false; if (criteria.status === "active" && p.dateTrashed) return false; @@ -550,11 +681,67 @@ const ProjectsPage = ({ contentContainerRef }) => { return false; } - // Search criteria for filterText - redundant with individual search - // criteria in FilterDrawer, and we could get rid of the search box - // above the grid. + if ( + criteria.nameList.length > 0 && + !criteria.nameList + .map(n => n.toLowerCase()) + .includes(p.name.toLowerCase()) + ) { + return false; + } + + if ( + criteria.addressList.length > 0 && + !criteria.addressList + .map(n => n.toLowerCase()) + .includes(p.address.toLowerCase()) + ) { + return false; + } + + if ( + criteria.alternativeList.length > 0 && + !criteria.alternativeList + .map(n => n.toLowerCase()) + .includes(p.alternative.toLowerCase()) + ) { + return false; + } + + if ( + criteria.authorList.length > 0 && + !criteria.authorList + .map(n => n.toLowerCase()) + .includes(p.fullname.toLowerCase()) + ) { + return false; + } + + if (criteria.dro && !p.dro.includes(criteria.dro)) return false; + + if ( + criteria.adminNotes && + !p.adminNotes.toLowerCase().includes(criteria.adminNotes.toLowerCase()) + ) { + return false; + } + + if ( + criteria.startDateModifiedAdmin && + getDateOnly(p.dateModifiedAdmin) < + getDateOnly(criteria.startDateModifiedAdmin) + ) + return false; + + if ( + criteria.endDateModifiedAdmin && + getDateOnly(p.dateModifiedAdmin) > + getDateOnly(criteria.endDateModifiedAdmin) + ) + return false; + if (filterText !== "") { - let ids = ["name", "address", "fullName", "alternative"]; + let ids = ["name", "address", "fullName", "alternative", "description"]; return ids.some(id => { let colValue = String(p[id]).toLowerCase(); @@ -565,6 +752,35 @@ const ProjectsPage = ({ contentContainerRef }) => { return true; }; + const resetFiltersSort = () => { + setCriteria({ + type: "all", + status: "active", + visibility: "visible", + name: "", + address: "", + author: "", + alternative: "", + dro: "", + startDateCreated: null, + endDateCreated: null, + startDateModified: null, + endDateModified: null, + nameList: [], + addressList: [], + alternativeList: [], + authorList: [], + droList: [], + adminNotes: "", + startDateModifiedAdmin: null, + endDateModifiedAdmin: null + }); + setOrderBy("dateModified"); + setOrder("desc"); + setCheckedProjectIds([]); + setSelectAllChecked(false); + }; + const headerData = [ { id: "checkAllProjects", @@ -581,18 +797,59 @@ const ProjectsPage = ({ contentContainerRef }) => { }, { id: "dateHidden", - label: "Visibility" + label: "Visibility", + popupType: "visibility" }, { id: "dateSnapshotted", - label: "Status" + label: "Status", + popupType: "status" + }, + { id: "name", label: "Name", popupType: "text" }, + { id: "address", label: "Address", popupType: "text" }, + { id: "alternative", label: "Alternative Number", popupType: "text" }, + { id: "author", label: "Created By", popupType: "text" }, + { + id: "dateCreated", + label: "Created On", + popupType: "datetime", + startDatePropertyName: "startDateCreated", + endDatePropertyName: "endDateCreated" + }, + { + id: "dateModified", + label: "Last Modified", + popupType: "datetime", + startDatePropertyName: "startDateModified", + endDatePropertyName: "endDateModified" + }, + { + id: "dateSubmitted", + label: "Submitted", + popupType: "datetime", + startDatePropertyName: "startDateSubmitted", + endDatePropertyName: "endDateSubmitted" }, - { id: "name", label: "Name" }, - { id: "address", label: "Address" }, - { id: "VERSION_NO", label: "Alternative Number" }, - { id: "firstName", label: "Created By" }, - { id: "dateCreated", label: "Created On" }, - { id: "dateModified", label: "Last Modified" }, + { + id: "dro", + label: "DRO", + popupType: null // temporarily disable filtering by DRO, as it crashes + }, + + ...(userContext.account?.isAdmin + ? [ + { + id: "adminNotes", + label: "Admin Notes", + popupType: null // No filter needed for this column + }, + { + id: "dateModifiedAdmin", + label: "Date Admin Modified", + popupType: "datetime" + } + ] + : []), { id: "contextMenu", label: "" @@ -602,7 +859,7 @@ const ProjectsPage = ({ contentContainerRef }) => { const indexOfLastPost = currentPage * projectsPerPage; const indexOfFirstPost = indexOfLastPost - projectsPerPage; const sortedProjects = stableSort( - projects.filter(filterProjects), + projects.filter(p => filter(p, criteria)), getComparator(order, orderBy) ); const currentProjects = sortedProjects.slice( @@ -613,7 +870,7 @@ const ProjectsPage = ({ contentContainerRef }) => { return (
-
{ setCollapsed={setFilterCollapsed} setCheckedProjectIds={setCheckedProjectIds} setSelectAllChecked={setSelectAllChecked} + droOptions={droOptions} /> -
+
*/}

Projects

@@ -645,7 +903,7 @@ const ProjectsPage = ({ contentContainerRef }) => { style={{ display: "flex", flexDirection: "row", - justifyContent: "space-between" + justifyContent: "flex-start" }} > { style={{ display: "flex", flexDirection: "row", - alignSelf: "flex-end" + alignSelf: "flex-end", + marginLeft: "20px", + justifyContent: "space-between", + width: "80%" }} >
@@ -670,26 +931,30 @@ const ProjectsPage = ({ contentContainerRef }) => { type="search" id="filterText" name="filterText" - placeholder="Search" + placeholder="Search by Name; Address; Description; Alt#" value={filterText} onChange={e => handleFilterTextChange(e.target.value)} /> - Search Icon +
- {filterCollapsed ? ( +
- ) : null} + {/* {filterCollapsed ? ( + + ) : null} */} +
@@ -697,37 +962,20 @@ const ProjectsPage = ({ contentContainerRef }) => { {headerData.map(header => { - const label = header.label; return ( - handleSort(header.id) - } - > - {orderBy === header.id ? ( - - {label}{" "} - {order === "asc" ? ( - - ) : ( - - )} - - ) : ( - {label} - )} + + ); })} @@ -746,9 +994,19 @@ const ProjectsPage = ({ contentContainerRef }) => { handleRenameSnapshotModalOpen={ handleRenameSnapshotModalOpen } + handleShareSnapshotModalOpen={ + handleShareSnapshotModalOpen + } handleHide={handleHide} handleCheckboxChange={handleCheckboxChange} checkedProjectIds={checkedProjectIds} + isAdmin={isAdmin} + droOptions={droOptions} + onDroChange={handleDroChange} // Pass the DRO change handler + onAdminNoteUpdate={handleAdminNoteUpdate} // Pass the admin note update handler + droName={ + isAdmin ? null : droNameMap[project.droId] || "N/A" + } // Pass the droName /> )) ) : ( @@ -769,34 +1027,14 @@ const ProjectsPage = ({ contentContainerRef }) => { currentPage={currentPage} maxNumOfVisiblePages={5} /> - + handlePerPageChange(e.target.value)} + name="perPage" + className={classes.dropContent} + /> + Items per page
{(selectedProject || checkedProjectsStatusData) && ( @@ -829,6 +1067,11 @@ const ProjectsPage = ({ contentContainerRef }) => { onClose={handleRenameSnapshotModalClose} selectedProjectName={selectedProjectName} /> + )}
diff --git a/client/src/components/Projects/RenameSnapshotModal.js b/client/src/components/Projects/RenameSnapshotModal.jsx similarity index 100% rename from client/src/components/Projects/RenameSnapshotModal.js rename to client/src/components/Projects/RenameSnapshotModal.jsx diff --git a/client/src/components/Projects/ShareSnapshotModal.jsx b/client/src/components/Projects/ShareSnapshotModal.jsx new file mode 100644 index 00000000..46d856c3 --- /dev/null +++ b/client/src/components/Projects/ShareSnapshotModal.jsx @@ -0,0 +1,406 @@ +import React, { useState, useEffect } from "react"; +import PropTypes from "prop-types"; +import { createUseStyles, useTheme } from "react-jss"; +import Button from "../Button/Button"; +import ModalDialog from "../UI/AriaModal/ModalDialog"; +import clsx from "clsx"; +import * as projectShareService from "../../services/projectShare.service"; + +const useStyles = createUseStyles(theme => ({ + buttonFlexBox: { + display: "flex", + flexDirection: "row", + padding: "1em" + }, + heading1: theme.typography.heading1, + heading2: theme.typography.heading2, + buttonColor: { + backgroundColor: theme.colors.secondary.lightGray + }, + buttonDisabled: { + cursor: "default" + }, + modal: { + width: "40em", + margin: "0 auto" + }, + input: { + padding: "0 5em", + margin: "1.5rem 2.5rem 1.5rem 0.75rem" + }, + emailList: { + height: "15em", + overflowY: "scroll", + lineHeight: "3em", + padding: "10px" + }, + emptyList: { + display: "flex", + height: "15em", + border: "solid black 2px", + margin: "1em", + alignItems: "center", + fontSize: "18px", + fontWeight: "bold", + padding: "0em 2em" + }, + viewPermissionsList: { + padding: "0em 4em" + }, + email: { + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + padding: "0px 12px", + fontSize: "18px", + "&:hover": { + backgroundColor: "#f2f2f2" + } + }, + copyMessageBox: { + border: "solid 2px", + margin: "1em", + borderRadius: "8px", + display: "flex", + flexDirection: "column" + }, + copyButton: { + backgroundColor: "darkBlue", + color: "white", + width: "100%", + margin: "0", + borderRadius: "0 0 6px 6px", + border: "none", + fontSize: "20px" + }, + remove: { + border: "none", + background: "none", + color: "red", + fontSize: "18px", + fontWeight: "normal", + cursor: "pointer" + }, + lineBreak: { + textAlign: "center", + borderBottom: "1px solid #000", + lineHeight: "0em", + margin: "10px 0 20px", + color: "black" + }, + popupMessage: { + position: "absolute", + bottom: "-10%", + borderRadius: "10px", + padding: "14px", + fontWeight: "bold", + fontSize: "18px", + border: "1px solid #d8dce3", + boxSizing: "border-box", + boxShadow: "0px 5px 10px rgba(0, 46, 109, 0.5)" + } +})); + +export default function ShareSnapshotModal({ mounted, onClose, project }) { + const theme = useTheme(); + const classes = useStyles({ theme }); + const [page, setPage] = useState(1); + const [sharedEmails, setSharedEmails] = useState([]); + const [selectedEmail, setSelectedEmail] = useState(null); + const [isCopied, setIsCopied] = useState(false); + const maybeDisabled = !sharedEmails.length && classes.buttonDisabled; + const tdmLink = "https://tdm-dev.azurewebsites.net"; + const copyLink = `${tdmLink}/projects/${project ? project.id : -1}`; + const copyMessage = `Here's a snapshot of the current TDM Calculator plan for: [${ + project ? project.name : "" + }](${copyLink}). \ +If you don't already have a [TDM Calculator](${tdmLink}) account, please set one up to see the above snapshot link.`; + + const fetchProjectShareList = async () => { + const response = await projectShareService.getByProjectId(project.id); + setSharedEmails(response.data); + }; + + useEffect(() => { + if (project) { + fetchProjectShareList().catch(console.error); + } + }, [project, setSharedEmails]); // eslint-disable-line react-hooks/exhaustive-deps + + const shareProject = async (email, project) => { + if (email && project) { + let newProjectShare = { + email: email, + projectId: project.id + }; + await projectShareService.post(newProjectShare).catch(console.error); + await fetchProjectShareList().catch(console.error); + } + }; + + const deleteProjectShare = async projectShare => { + if (projectShare) { + await projectShareService.del(projectShare.id).catch(console.error); + await fetchProjectShareList().catch(console.error); + } + }; + + const handleSubmitEmail = e => { + switch (e.key) { + case "Enter": + shareProject(e.target.value, project); + } + }; + + const closeProject = () => { + onClose(); + setPage(1); + setSelectedEmail(null); + setIsCopied(false); + }; + + const modalContents = page => { + switch (Number(page)) { + case 1: + return ( +
+
+ Share "{project ? project.name : ""}" Snapshot? +
+
+ { + handleSubmitEmail(e); + }} + /> +
+
+
+ People with viewing permission +
+ {sharedEmails.length ? ( +
+ {sharedEmails.map(email => ( +
+ {email.email} + +
+ ))} +
+ ) : ( +
+ No email addresses added. Please include at least one email to + share the project. +
+ )} +
+ +
+
+
+ ); + case 2: + return ( +
+
+
+ Share "{project ? project.name : ""}" Snapshot +
+
+
+ Copy link with message +
+
+

+ Here's a snapshot of the current TDM Calculator plan + for: {project.name}.

+

If you don't already have a{" "} + TDM Calculator account, please set one + up to see the above snapshot link. +

+ +
+
+ + OR + +
+
+ Copy link +
+
+

+ {copyLink} +

+ +
+
+
+ + +
+
+
Successfully Copied!
+
+
+
+ ); + case 3: + return ( +
+
+ Are you sure? +
+
+
+ Are you sure you want to remove {selectedEmail.email} from + viewing this snapshot? +
+
+ + +
+
+
+ ); + } + }; + + return ( +
+ { + closeProject(); + }} + initialFocus="#emailAddresses" + > + {modalContents(page)} + +
+ ); +} + +ShareSnapshotModal.propTypes = { + mounted: PropTypes.bool, + onClose: PropTypes.func, + project: PropTypes.any +}; diff --git a/client/src/components/Projects/SnapshotProjectModal.js b/client/src/components/Projects/SnapshotProjectModal.js index 72630c41..ac2a162b 100644 --- a/client/src/components/Projects/SnapshotProjectModal.js +++ b/client/src/components/Projects/SnapshotProjectModal.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { createUseStyles, useTheme } from "react-jss"; import Button from "../Button/Button"; -import { MdFileCopy } from "react-icons/md"; +import { MdCameraAlt } from "react-icons/md"; import ModalDialog from "../UI/AriaModal/ModalDialog"; const useStyles = createUseStyles(theme => ({ @@ -16,6 +16,9 @@ const useStyles = createUseStyles(theme => ({ heading1: theme.typography.heading1, buttonColor: { backgroundColor: "#eaeff2" + }, + camera: { + marginBottom: "-5px" } })); @@ -37,7 +40,7 @@ export default function SnapshotProjectModal({ initialFocus="#duplicateName" >
- Convert " + Convert " {`${selectedProjectName}`}" Into a Snapshot?
diff --git a/client/src/components/Projects/csvData.js b/client/src/components/Projects/csvData.jsx similarity index 100% rename from client/src/components/Projects/csvData.js rename to client/src/components/Projects/csvData.jsx diff --git a/client/src/components/Roles.js b/client/src/components/Roles.jsx similarity index 95% rename from client/src/components/Roles.js rename to client/src/components/Roles.jsx index 5b958867..5e0a6c80 100644 --- a/client/src/components/Roles.js +++ b/client/src/components/Roles.jsx @@ -105,7 +105,7 @@ const Roles = ({ contentContainerRef }) => { const classes = useStyles(); const toast = useToast(); const userContext = useContext(UserContext); - const loggedInUserId = userContext.account.id; + const loggedInUserId = userContext.account?.id; useEffect(() => { const getAccounts = async () => { @@ -295,17 +295,17 @@ const Roles = ({ contentContainerRef }) => { trigger={ } position="bottom center" @@ -313,7 +313,7 @@ const Roles = ({ contentContainerRef }) => { on="click" closeOnDocumentClick arrow={false} - onOpen={() => setHoveredRow(account.id)} + onOpen={() => setHoveredRow(account?.id)} onClose={() => setHoveredRow(null)} >
diff --git a/client/src/components/SubmitSnapshot/SubmitSnapshotModal.jsx b/client/src/components/SubmitSnapshot/SubmitSnapshotModal.jsx new file mode 100644 index 00000000..4a19b7c3 --- /dev/null +++ b/client/src/components/SubmitSnapshot/SubmitSnapshotModal.jsx @@ -0,0 +1,74 @@ +import React from "react"; +import { createUseStyles, useTheme } from "react-jss"; +import ModalDialog from "../UI/AriaModal/ModalDialog"; +import Button from "../Button/Button"; +import PropTypes from "prop-types"; +import { IoMdCheckmarkCircle } from "react-icons/io"; + +const useStyles = createUseStyles(theme => ({ + buttonFlexBox: { + display: "flex", + flexDirection: "row", + justifyContent: "center", + margin: 0 + }, + heading1: theme.typography.heading1, + buttonColor: { + backgroundColor: "#eaeff2" + }, + circleCheck: { + marginTop: "1.5rem", + marginBottom: "1.5rem", + width: "80px", + height: "80px", + color: "green" + } +})); + +export default function SubmitSnapshotModal({ mounted, onClose }) { + const theme = useTheme(); + const classes = useStyles({ theme }); + + return ( + + <> +
+ +
+
+
+ Ready for your submission! +
+
+ Are you sure you want to submit the Snapshot? +
+
+ + +
+ + +
+
+ ); +} + +SubmitSnapshotModal.propTypes = { + mounted: PropTypes.bool, + onClose: PropTypes.func +}; diff --git a/client/src/components/SubmitSnapshot/SubmitSnapshotPage.jsx b/client/src/components/SubmitSnapshot/SubmitSnapshotPage.jsx new file mode 100644 index 00000000..dc81619a --- /dev/null +++ b/client/src/components/SubmitSnapshot/SubmitSnapshotPage.jsx @@ -0,0 +1,238 @@ +import React, { useState, useContext } from "react"; +import PropTypes from "prop-types"; +import { useNavigate } from "react-router-dom"; +import { createUseStyles } from "react-jss"; +import ContentContainer from "../Layout/ContentContainer"; +import Button from "../Button/Button"; +import SubmitSnapshotModal from "./SubmitSnapshotModal.jsx"; +import SubmitSnapshotTable from "./SubmitSnapshotTable"; +import useErrorHandler from "../../hooks/useErrorHandler"; +import useProjects from "../../hooks/useGetProjects"; +import UserContext from "../../contexts/UserContext"; +import * as projectService from "../../services/project.service.js"; +import * as projectResultService from "../../services/projectResult.service"; + +const useStyles = createUseStyles({ + pageTitle: { + marginBottom: "16px", + textAlign: "center" + }, + subtitle: { + marginTop: "0px", + textAlign: "center", + padding: 0 + }, + submitButton: { + marginTop: "1em", + alignSelf: "flex-end" + }, + heading3: { + fontWeight: "bold", + fontSize: "20px", + lineHeight: "140%" + }, + tableHead: { + textAlign: "left" + }, + tableCell: { + padding: "0em 1em 0em 0em" + } +}); + +const SubmitSnapshotPage = props => { + const { contentContainerRef } = props; + const classes = useStyles(); + const userContext = useContext(UserContext); + const email = userContext.account ? userContext.account.email : ""; + const navigate = useNavigate(); + const handleError = useErrorHandler(email, navigate); + const [projects, setProjects] = useProjects(handleError); + const [selectedProjectId, setSelectedProjectId] = useState(null); + const [selectedProject, setSelectedProject] = useState(null); + const [submissionModalOpen, setSubmissionModalOpen] = useState(false); + + const eligibleProjects = projects.filter( + p => + p.earnedPoints >= p.targetPoints && p.dateSnapshotted && !p.dateSubmitted + ); + const ineligibleProjects = projects.filter( + p => + p.earnedPoints < p.targetPoints && p.dateSnapshotted && !p.dateSubmitted + ); + const submittedProjects = projects.filter(p => p.dateSubmitted); + + const updateProjects = async () => { + let response = await projectService.get(); + let projects = response.data.filter( + p => p.loginId === userContext.account.loginId && p.dateSnapshotted + ); + const projectsNeedingCalculation = projects.filter(p => !p.targetPoints); + // The following code determines if the targetPoints, earnedPoints or projectLevel + // columns in the project table have not been calculated. If this is the case, they + // are calculated, saved to the db, and the projects are re-fetched. This should + // only apply to legacy projects. New projects should have these fields saved + // when the project is saved. + if (projectsNeedingCalculation.length > 0) { + for (let i = 0; i < projectsNeedingCalculation.length; i++) { + await projectResultService.populateTargetPoints( + projectsNeedingCalculation[i] + ); + } + response = await projectService.get(); + projects = response.data.filter( + p => p.loginId === userContext.account.loginId && p.dateSnapshotted + ); + } + + setProjects(projects); + }; + + const handleSelectProject = projectId => { + setSelectedProjectId(projectId); + setSelectedProject(projects.find(p => p.id === projectId)); + }; + + const handleSubmissionModalOpen = () => { + if (!selectedProject) return; + setSubmissionModalOpen(true); + }; + + const handleSubmissionModalClose = async action => { + if (action === "ok") { + try { + await projectService.submit({ id: selectedProject.id }); + await updateProjects(); + } catch (err) { + handleError(err); + } + } + setSubmissionModalOpen(false); + setSelectedProject(null); + setSelectedProjectId(null); + }; + + return ( + +
+

Snapshot Submittal Form

+
+

+ This form is for submitting a snapshot to the LA City Planning + Department for review. +

+

Snapshots must meet target points before they can be submitted

+
+
+
Target Points
+ +

Snapshots Meeting Target Points:

+ +
+ + + + + + + + + + + + + {eligibleProjects.map(project => + project.dateSnapshotted ? ( + + ) : null + )} + +
NameAddressDate SubmittedDate Modified
+
+ +

Snapshots Not Meeting Target Points:

+ +
+ + + + + + + + + + + + {ineligibleProjects.map(project => + project.dateSnapshotted ? ( + + ) : null + )} + +
NameAddressDate SubmittedDate Modified
+
+ +

Submitted Snapshots:

+ +
+ + + + + + + + + + + + {submittedProjects.map(project => + project.dateSnapshotted ? ( + + ) : null + )} + +
NameAddressDate SubmittedDate Modified
+
+ +
+ + + +
+
+ ); +}; + +SubmitSnapshotPage.propTypes = { + contentContainerRef: PropTypes.object +}; + +export default SubmitSnapshotPage; diff --git a/client/src/components/SubmitSnapshot/SubmitSnapshotTable.jsx b/client/src/components/SubmitSnapshot/SubmitSnapshotTable.jsx new file mode 100644 index 00000000..7e7023c2 --- /dev/null +++ b/client/src/components/SubmitSnapshot/SubmitSnapshotTable.jsx @@ -0,0 +1,59 @@ +import React from "react"; +import { createUseStyles } from "react-jss"; +import PropTypes from "prop-types"; +import { formatDatetime } from "../../helpers/util"; + +const useStyles = createUseStyles({ + heading3: { + fontWeight: "bold", + fontSize: "20px", + lineHeight: "140%" + }, + tableHead: { + textAlign: "left" + }, + tableCell: { + padding: "0em 1em 0em 0em" + } +}); + +const SubmitSnapshotTable = ({ + project, + handleSelectProject, + selectedProjectId, + includeRadioButton +}) => { + const classes = useStyles(); + + return ( + + {includeRadioButton && ( + + handleSelectProject(project.id)} + /> + + )} + {project.name} + {project.address} + + {formatDatetime(project.dateSubmitted)} + + + {formatDatetime(project.dateModified)} + + + ); +}; + +SubmitSnapshotTable.propTypes = { + rules: PropTypes.array, + project: PropTypes.any, + handleSelectProject: PropTypes.func, + selectedProjectId: PropTypes.number, + includeRadioButton: PropTypes.bool +}; + +export default SubmitSnapshotTable; diff --git a/client/src/components/TermsAndConditions/TermsAndConditionsContent.js b/client/src/components/TermsAndConditions/TermsAndConditionsContent.jsx similarity index 100% rename from client/src/components/TermsAndConditions/TermsAndConditionsContent.js rename to client/src/components/TermsAndConditions/TermsAndConditionsContent.jsx diff --git a/client/src/components/TermsAndConditions/TermsAndConditionsModal.js b/client/src/components/TermsAndConditions/TermsAndConditionsModal.jsx similarity index 100% rename from client/src/components/TermsAndConditions/TermsAndConditionsModal.js rename to client/src/components/TermsAndConditions/TermsAndConditionsModal.jsx diff --git a/client/src/components/TermsAndConditions/TermsAndConditionsPage.js b/client/src/components/TermsAndConditions/TermsAndConditionsPage.jsx similarity index 100% rename from client/src/components/TermsAndConditions/TermsAndConditionsPage.js rename to client/src/components/TermsAndConditions/TermsAndConditionsPage.jsx diff --git a/client/src/components/ToolTip/AccordionToolTip.js b/client/src/components/ToolTip/AccordionToolTip.jsx similarity index 72% rename from client/src/components/ToolTip/AccordionToolTip.js rename to client/src/components/ToolTip/AccordionToolTip.jsx index 67e60a92..5422a4f9 100644 --- a/client/src/components/ToolTip/AccordionToolTip.js +++ b/client/src/components/ToolTip/AccordionToolTip.jsx @@ -2,6 +2,8 @@ import React from "react"; import PropTypes from "prop-types"; import { createUseStyles } from "react-jss"; import clsx from "clsx"; +import { Interweave } from "interweave"; +import { MdLaunch } from "react-icons/md"; const useStyles = createUseStyles({ accordionTooltipLabel: { @@ -69,18 +71,32 @@ const AccordionToolTip = ({ classes.accordionTooltipLabel, classes.disabledDescription )} - dangerouslySetInnerHTML={{ __html: `${description}` }} - >
+ > + +
) : ( -
+
+ +
)} ); }; +function TransformExternalLink(node, children) { + const classes = useStyles(); + if (node.tagName == "A" && !node.getAttribute("href").startsWith("/")) { + return ( + + + {children} + + + + ); + } +} + AccordionToolTip.propTypes = { description: PropTypes.string, setShowDescription: PropTypes.func, diff --git a/client/src/components/ToolTip/ToolTip.js b/client/src/components/ToolTip/ToolTip.jsx similarity index 100% rename from client/src/components/ToolTip/ToolTip.js rename to client/src/components/ToolTip/ToolTip.jsx diff --git a/client/src/components/ToolTip/ToolTipIcon.js b/client/src/components/ToolTip/ToolTipIcon.jsx similarity index 100% rename from client/src/components/ToolTip/ToolTipIcon.js rename to client/src/components/ToolTip/ToolTipIcon.jsx diff --git a/client/src/components/ToolTip/ToolTipLabel.js b/client/src/components/ToolTip/ToolTipLabel.jsx similarity index 98% rename from client/src/components/ToolTip/ToolTipLabel.js rename to client/src/components/ToolTip/ToolTipLabel.jsx index 3ce55214..b209c47d 100644 --- a/client/src/components/ToolTip/ToolTipLabel.js +++ b/client/src/components/ToolTip/ToolTipLabel.jsx @@ -193,13 +193,13 @@ const ToolTipLabel = ({ ToolTipLabel.propTypes = { id: PropTypes.string.isRequired, - tooltipContent: PropTypes.string.isRequired, + tooltipContent: PropTypes.any.isRequired, children: PropTypes.node.isRequired, code: PropTypes.string, requiredInput: PropTypes.bool, disabledInput: PropTypes.bool, setShowDescription: PropTypes.func, - description: PropTypes.string, + description: PropTypes.any, showDescription: PropTypes.bool }; diff --git a/client/src/components/UI/AriaModal/ModalDialog.js b/client/src/components/UI/AriaModal/ModalDialog.jsx similarity index 97% rename from client/src/components/UI/AriaModal/ModalDialog.js rename to client/src/components/UI/AriaModal/ModalDialog.jsx index cd3f685c..b61ca358 100644 --- a/client/src/components/UI/AriaModal/ModalDialog.js +++ b/client/src/components/UI/AriaModal/ModalDialog.jsx @@ -2,7 +2,7 @@ import React from "react"; import PropTypes from "prop-types"; import { createUseStyles } from "react-jss"; import AriaModal from "react-aria-modal"; -import { FaX } from "react-icons/fa6"; +import { MdClose } from "react-icons/md"; const useStyles = createUseStyles({ modalContainer: { @@ -89,7 +89,7 @@ export default function ModalDialog({ className={classes.closeButton} aria-label={`Close ${title} modal`} > - +
)} diff --git a/client/src/components/UI/DatePickerCustomInput.jsx b/client/src/components/UI/DatePickerCustomInput.jsx index 13007023..31a099c3 100644 --- a/client/src/components/UI/DatePickerCustomInput.jsx +++ b/client/src/components/UI/DatePickerCustomInput.jsx @@ -2,7 +2,7 @@ import React, { forwardRef } from "react"; import PropTypes from "prop-types"; const DatePickerCustomInput = forwardRef( - ({ value, onClick, onChange }, ref) => ( + ({ value, onClick, onChange, placeholder }, ref) => ( ) ); @@ -23,7 +24,8 @@ DatePickerCustomInput.displayName = "DatePickerCustomInput"; DatePickerCustomInput.propTypes = { value: PropTypes.any, onClick: PropTypes.func, - onChange: PropTypes.func + onChange: PropTypes.func, + placeholder: PropTypes.string }; export default DatePickerCustomInput; diff --git a/client/src/components/UI/DateRangePicker.jsx b/client/src/components/UI/DateRangePicker.jsx index f4682125..8487d68e 100644 --- a/client/src/components/UI/DateRangePicker.jsx +++ b/client/src/components/UI/DateRangePicker.jsx @@ -1,44 +1,102 @@ -import React from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; import DatePicker from "react-datepicker"; import DatePickerCustomInput from "./DatePickerCustomInput"; -const DateRangePicker = ({ startDate, endDate, setStartDate, setEndDate }) => { +// TODO: we want the calendar popup to appear as if it were in the normal CSS flow. +// We should be able to do this with a React portal, but I (John Darragh) was not +// able to get this to work (see https://reactdatepicker.com/#example-date-range-with-portalInstead +// for the example that should work). Instead, we use a total hack by putting a spacer div below +// the datepickers, and conditionally showi it if the calendar is open. Would be +// good to replace this with something more solid, cause it somethimes doesn't work well +// e.g. if the popup doesn't have enough space in the broswser window below the datepicker, +// the popup appears above the date picker and is then not positioned over the +// spacer div. + +const DateRangePicker = ({ + startDate, + endDate, + setStartDate, + setEndDate, + startDatePlaceholder, + endDatePlaceholder +}) => { + const [calendarDivHeight, setCalendarDivHeight] = useState(false); + const handleCalendarOpen = () => { + setCalendarDivHeight(true); + }; + + const handleCalendarClose = () => { + setCalendarDivHeight(false); + }; + return ( -
- setStartDate(date)} - startDate={startDate} - endDate={endDate} - customInput={} - dateFormat="yyyy-MM-dd" - /> -
 to 
- setEndDate(date)} - startDate={startDate} - endDate={endDate} - customInput={} - dateFormat="yyyy-MM-dd" - /> -
+ <> +
{ + // We don't want a click event to bubble up to the parent container + e.stopPropagation(); + }} + > + setStartDate(date)} + startDate={startDate} + endDate={endDate} + customInput={} + dateFormat="yyyy-MM-dd" + placeholderText={startDatePlaceholder || ""} + popperPlacement="bottom-start" + onCalendarOpen={handleCalendarOpen} + onCalendarClose={handleCalendarClose} + onClick={e => { + // We don't want a click event to bubble up to the parent container + e.stopPropagation(); + }} + // withPortal + /> +
+ to +
+ setEndDate(date)} + startDate={startDate} + endDate={endDate} + customInput={} + dateFormat="yyyy-MM-dd" + placeholderText={endDatePlaceholder || ""} + popperPlacement="bottom-end" + onCalendarOpen={handleCalendarOpen} + onCalendarClose={handleCalendarClose} + // withPortal + /> +
+ {calendarDivHeight ? ( +
{calendarDivHeight}
+ ) : null} + ); }; DateRangePicker.propTypes = { + portalId: PropTypes.string, startDate: PropTypes.any, endDate: PropTypes.any, setStartDate: PropTypes.func, - setEndDate: PropTypes.func + setEndDate: PropTypes.func, + startDatePlaceholder: PropTypes.string, + endDatePlaceholder: PropTypes.string }; export default DateRangePicker; diff --git a/client/src/components/UI/Pagination.js b/client/src/components/UI/Pagination.jsx similarity index 100% rename from client/src/components/UI/Pagination.js rename to client/src/components/UI/Pagination.jsx diff --git a/client/src/components/UI/Quill.js b/client/src/components/UI/Quill.jsx similarity index 100% rename from client/src/components/UI/Quill.js rename to client/src/components/UI/Quill.jsx diff --git a/client/src/components/UI/UniversalSelect.jsx b/client/src/components/UI/UniversalSelect.jsx new file mode 100644 index 00000000..9c43c43c --- /dev/null +++ b/client/src/components/UI/UniversalSelect.jsx @@ -0,0 +1,128 @@ +import React from "react"; +import Select from "react-select"; +import PropTypes from "prop-types"; +import classNames from "classnames"; + +UniversalSelect.propTypes = { + value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired + }) + ).isRequired, + onChange: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + id: PropTypes.string, + disabled: PropTypes.bool, + autoFocus: PropTypes.bool, + className: PropTypes.string +}; + +export default function UniversalSelect({ + value, + defaultValue, + options, + onChange, + name, + id, + disabled, + autoFocus, + className +}) { + // To make UniversalSelect compatible with a standard react component, value should be passed the value of + // the selected option. + value={options.find(o => o.value == value) || ""} + defaultValue={defaultValue} + name={name} + inputId={id} + isDisabled={disabled} + options={options} + styles={{ + container: (provided, state) => ({ + ...provided, + border: + state.isFocused || state.menuIsOpen + ? "1px solid black" + : "1px solid gray", + boxShadow: + state.isFocused || state.menuIsOpen ? "none" : provided.boxShadow, + outline: "none", + borderRadius: "3px", + padding: 0 + }), + option: (provided, state) => ({ + ...provided, + fontSize: "15px", // Set your desired font size for options + padding: "2px", // Set padding to 0 for options + backgroundColor: state.isSelected + ? "#1967D2ff" + : state.isFocused + ? "#e5e7eb" + : "white", // Change background color based on state + color: state.isSelected ? "white" : "black", // Ensure text is readable on dark background, + boxShadow: "10px" + }), + singleValue: provided => ({ + ...provided, + fontSize: "15px", + color: "black" + }), + menu: provided => ({ + ...provided, + padding: "0", // Set padding of the dropdown menu to 0 + border: "1px solid black", + boxShadow: "none", + marginTop: "1px", + borderRadius: "0" + }), + menuList: provided => ({ + ...provided, + padding: 0 + }), + control: provided => ({ + ...provided, + padding: "0", + minHeight: "auto", + border: "none", + boxShadow: "none", + outline: "none" + }), + dropdownIndicator: provided => ({ + ...provided, + padding: "2px" + }), + clearIndicator: provided => ({ + ...provided, + padding: "2px" + }), + valueContainer: provided => ({ + ...provided, + paddingLeft: "4px", + padding: "0" + }), + indicatorSeparator: provided => ({ + ...provided, + display: "none" + }) + }} + /> + ); +} diff --git a/client/src/helpers/Config.js b/client/src/helpers/Config.js index 61572133..d92554d7 100644 --- a/client/src/helpers/Config.js +++ b/client/src/helpers/Config.js @@ -1,4 +1,4 @@ -import * as configService from "../services/config.service.js"; +import * as configService from "../services/config.service"; export const getConfigs = async () => { try { diff --git a/client/src/helpers/Constants.js b/client/src/helpers/Constants.js new file mode 100644 index 00000000..35e2185e --- /dev/null +++ b/client/src/helpers/Constants.js @@ -0,0 +1 @@ +export const ENABLE_UPDATE_TOTALS = false; diff --git a/client/src/hooks/useGetProjects.js b/client/src/hooks/useGetProjects.js index 0ee56389..b1542b08 100644 --- a/client/src/hooks/useGetProjects.js +++ b/client/src/hooks/useGetProjects.js @@ -18,6 +18,8 @@ const useProjects = handleError => { (1000 * 60 * 60 * 24); if (numberOfDaysSinceTrashed && numberOfDaysSinceTrashed >= 90) { deleteOverNinety(cur.id, handleError); + } else { + fetchedProjects.push(cur); } } else { fetchedProjects.push(cur); diff --git a/client/src/images/close.png b/client/src/images/close.png deleted file mode 100644 index f2543d64..00000000 Binary files a/client/src/images/close.png and /dev/null differ diff --git a/client/src/images/copy.png b/client/src/images/copy.png deleted file mode 100644 index d8d8e0cb..00000000 Binary files a/client/src/images/copy.png and /dev/null differ diff --git a/client/src/images/trash.png b/client/src/images/trash.png deleted file mode 100644 index 1e64dab5..00000000 Binary files a/client/src/images/trash.png and /dev/null differ diff --git a/client/src/services/dro.service.js b/client/src/services/dro.service.js new file mode 100644 index 00000000..0ce6bdd3 --- /dev/null +++ b/client/src/services/dro.service.js @@ -0,0 +1,27 @@ +import axios from "axios"; + +const baseUrl = "/api/dro/"; + +export function get() { + try { + return axios.get(baseUrl); + } catch (error) { + return new Promise.reject(error); + } +} + +export function getById(id) { + return axios.get(`${baseUrl}/${id}`); +} + +export function post(dro) { + return axios.post(baseUrl, dro); +} + +export function put(dro) { + return axios.put(baseUrl + "/" + dro.id, dro); +} + +export function del(id) { + return axios.delete(baseUrl + "/" + id); +} diff --git a/client/src/services/project.service.js b/client/src/services/project.service.js index cdeb587c..49f9fe74 100644 --- a/client/src/services/project.service.js +++ b/client/src/services/project.service.js @@ -19,19 +19,23 @@ export function post(project) { } export function put(project) { - return axios.put(baseUrl + "/" + project.id, project); + return axios.put(`${baseUrl}/${project.id}`, project); } export function del(id) { - return axios.delete(baseUrl + "/" + id); + return axios.delete(`${baseUrl}/${id}`); +} + +export function submit(id) { + return axios.put(`${baseUrl}/submit`, id); } export function snapshot(id) { - return axios.put(baseUrl + "/snapshot", id); + return axios.put(`${baseUrl}/snapshot`, id); } export function renameSnapshot(id) { - return axios.put(baseUrl + "/renameSnapshot", id); + return axios.put(`${baseUrl}/renameSnapshot`, id); } export function getAllArchivedProjects() { @@ -52,8 +56,22 @@ export function hide(projectIds, hide) { export function trash(projectIds, trash) { try { - return axios.put(`${baseUrl}/trash`, { ids: projectIds, trash }); + const response = axios.put(`${baseUrl}/trash`, { ids: projectIds, trash }); + return response; } catch (error) { return new Promise.reject(error); } } + +// New function to update the DRO ID +export function updateDroId(projectId, droId, loginId) { + return axios.put(`${baseUrl}/updateDroId/${projectId}`, { droId, loginId }); +} + +export function updateAdminNotes(id, adminNotes) { + return axios.put(`${baseUrl}/updateAdminNotes/${id}`, { adminNotes }); +} + +export function updateTotals(requestBody) { + return axios.put(`${baseUrl}/updateTotals/${requestBody.id}`, requestBody); +} diff --git a/client/src/services/projectResult.service.js b/client/src/services/projectResult.service.js index f53058c3..7e250d17 100644 --- a/client/src/services/projectResult.service.js +++ b/client/src/services/projectResult.service.js @@ -22,3 +22,23 @@ export async function getProjectResult(projectId) { const result = engine.showRulesArray(); return result; } + +// This is a temporary function to fill in three columns in project table for +// legacy projects +export async function populateTargetPoints(projectId) { + const projectResponse = await projectService.getById(projectId); + const project = projectResponse.data; + const rules = await getProjectResult(project.id); + const targetPoints = rules.find(r => r.code === "TARGET_POINTS_PARK").value; + const earnedPoints = rules.find(r => r.code === "PTS_EARNED").value; + const projectLevel = rules.find(r => r.code === "PROJECT_LEVEL").value; + + const requestBody = { + id: projectId, + targetPoints, + earnedPoints, + projectLevel + }; + + await projectService.updateTotals(requestBody); +} diff --git a/client/src/services/projectShare.service.js b/client/src/services/projectShare.service.js new file mode 100644 index 00000000..7401ba45 --- /dev/null +++ b/client/src/services/projectShare.service.js @@ -0,0 +1,19 @@ +import axios from "axios"; + +const baseUrl = "/api/projectShare"; + +export function getById(id) { + return axios.get(`${baseUrl}/${id}`); +} + +export function getByProjectId(projectId) { + return axios.get(`${baseUrl}/projectId/${projectId}`); +} + +export function post(projectShare) { + return axios.post(baseUrl, projectShare); +} + +export function del(id) { + return axios.delete(`${baseUrl}/${id}`); +} diff --git a/client/src/services/tdm-engine.test.js b/client/src/services/tdm-engine.test.js index 8417bd34..ba3fc68a 100644 --- a/client/src/services/tdm-engine.test.js +++ b/client/src/services/tdm-engine.test.js @@ -1,11 +1,7 @@ import Engine from "./tdm-engine"; import { engineTestRules, engineTestInput1 } from "../test-data/engine-test"; import { tdmRules } from "../test-data/tdm-calc-rules"; -import { - project1, - project2, - project3 -} from "../test-data/tdm-calc-examples.js"; +import { project1, project2, project3 } from "../test-data/tdm-calc-examples"; import "@testing-library/jest-dom"; describe("class Engine", () => { diff --git a/client/src/styles/theme.js b/client/src/styles/theme.js index 0b0cb130..b3f9409b 100644 --- a/client/src/styles/theme.js +++ b/client/src/styles/theme.js @@ -5,6 +5,8 @@ module.exports = { colorPrimary: "#a7c539", //lime green colorText: "#0F2940", //dark blue colorLADOT: "#002E6D", + colorGray: "#808080", + colorLightGray: "#A0A0A0", colorEarnedPoints: "rgb(255, 168, 4)", //orange colorDisabled: "rgba(0, 0, 0, .05)", //lightest grey transparent colorCancel: "rgba(0, 0, 0, 0.5)", //light grey, e.g. cancel button diff --git a/package-lock.json b/package-lock.json index 3f8ca731..d623e78e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "i": "^0.3.7", "luxon": "^3.4.4", "react-csv": "^2.2.2", + "react-icons": "^5.2.1", "wait-on": "^7.1.0" }, "devDependencies": { @@ -4254,7 +4255,6 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4801,6 +4801,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -6735,11 +6747,31 @@ "node": ">=8" } }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-csv": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz", "integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==" }, + "node_modules/react-icons": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", + "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", diff --git a/package.json b/package.json index 836ba3ad..70b509e6 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "i": "^0.3.7", "luxon": "^3.4.4", "react-csv": "^2.2.2", + "react-icons": "^5.2.1", "wait-on": "^7.1.0" }, "husky": { diff --git a/server/app/controllers/dro.controller.js b/server/app/controllers/dro.controller.js new file mode 100644 index 00000000..8347bbe8 --- /dev/null +++ b/server/app/controllers/dro.controller.js @@ -0,0 +1,72 @@ +const droService = require("../services/dro.service"); +const { + validate, + validationErrorMiddleware +} = require("../../middleware/validate"); +const droSchema = require("../schemas/dro"); + +const getAll = async (req, res) => { + try { + const droItems = await droService.getAll(); + res.status(200).json(droItems); + } catch (err) { + res.status(500).send(err); + } +}; + +const getById = async (req, res) => { + try { + const dro = await droService.getById(req.params.id); + if (!dro) { + return res.status(404).send("Dro not found."); + } + res.status(200).json(dro); + } catch (err) { + res.status(500).send(err); + } +}; + +const post = async (req, res) => { + try { + const response = await droService.post(req.body); + res.status(201).json(response); + } catch (err) { + res.status(500).send(err); + } +}; + +const put = async (req, res) => { + try { + const dro = await droService.getById(req.params.id); + if (!dro) { + return res.status(404).send("Dro not found."); + } + + await droService.put(req.body); + res.sendStatus(204); + } catch (err) { + res.status(500).send(err); + } +}; + +const del = async (req, res) => { + try { + const dro = await droService.getById(req.params.id); + if (!dro) { + return res.status(404).send("Dro not found."); + } + + await droService.del(req.params.id); + res.sendStatus(204); + } catch (err) { + res.status(500).send(err); + } +}; + +module.exports = { + getAll, + getById, + post: [validate({ body: droSchema }), post, validationErrorMiddleware], + put, + del +}; diff --git a/server/app/controllers/project.controller.js b/server/app/controllers/project.controller.js index e040f199..5e84751e 100644 --- a/server/app/controllers/project.controller.js +++ b/server/app/controllers/project.controller.js @@ -53,10 +53,47 @@ const put = async (req, res) => { } }; +const updateDroId = async (req, res) => { + try { + if (!req.user.isAdmin) { + return res + .status(403) + .json({ error: "You do not have permission to update the droId." }); + } + + const { id } = req.params; + const { droId } = req.body; + + await projectService.updateDroId(id, droId, req.user.id); + res.sendStatus(204); + } catch (err) { + res.status(500).send(err); + } +}; + +const updateAdminNotes = async (req, res) => { + try { + console.log(req); + if (!req.user.isAdmin) { + return res + .status(403) + .json({ error: "You do not have permission to update the droId." }); + } + + const { id } = req.params; + const { adminNotes } = req.body; + + await projectService.updateAdminNotes(id, adminNotes, req.user.id); + res.sendStatus(204); + } catch (err) { + res.status(500).send(err); + } +}; + const del = async (req, res) => { try { const project = await getProject(req, res); - if (project.loginId !== req.user.id) { + if (project.loginId !== req.user.id && !req.user.isAdmin) { res.status(403).send("You can only delete your own projects."); return; } @@ -107,6 +144,21 @@ const trash = async (req, res) => { } }; +const submit = async (req, res) => { + try { + const { id } = req.body; + + const result = await projectService.submit(id, req.user.id); + if (result === 1) { + res.sendStatus(403); + } else { + res.sendStatus(204); + } + } catch (err) { + res.status(500).send(err); + } +}; + const snapshot = async (req, res) => { try { const { id, name } = req.body; @@ -152,6 +204,28 @@ const getAllArchivedProjects = async (req, res) => { } }; +const updateTotals = async (req, res) => { + try { + if (!req.user.isAdmin) { + return res.status(403).json({ + error: "Access denied. Only admins can update totals." + }); + } + const { id, targetPoints, earnedPoints, projectLevel } = req.body; + const archivedProjects = await projectService.updateTotals( + id, + targetPoints, + earnedPoints, + projectLevel, + req.user.id + ); + res.status(200).json(archivedProjects); + return; + } catch (err) { + res.status(500).send(err); + } +}; + module.exports = { getAll, getById, @@ -160,7 +234,11 @@ module.exports = { del, hide, trash, + submit, snapshot, + updateDroId, + updateAdminNotes, renameSnapshot, - getAllArchivedProjects + getAllArchivedProjects, + updateTotals }; diff --git a/server/app/controllers/projectShare.controller.js b/server/app/controllers/projectShare.controller.js new file mode 100644 index 00000000..108318a4 --- /dev/null +++ b/server/app/controllers/projectShare.controller.js @@ -0,0 +1,66 @@ +const projectShareService = require("../services/projectShare.service"); +const { + validate, + validationErrorMiddleware +} = require("../../middleware/validate"); +const projectShareSchema = require("../schemas/projectShare"); + +const getById = async (req, res) => { + try { + const project = await projectShareService.getById(req.params.id); + if (!project) { + return res.status(404).send("Project not found."); + } + res.status(200).json(project); + } catch (err) { + res.status(500).send(err); + } +}; + +const getByProjectId = async (req, res) => { + try { + const sharedEmails = await projectShareService.getByProjectId( + req.params.projectId + ); + if (!sharedEmails) { + return res.status(404).send("Project not found."); + } + res.status(200).json(sharedEmails); + } catch (err) { + res.status(500).send(err); + } +}; + +const post = async (req, res) => { + try { + const response = await projectShareService.post(req.body); + res.status(201).json(response); + } catch (err) { + res.status(500).send(err); + } +}; + +const del = async (req, res) => { + try { + const projectShare = await projectShareService.getById(req.params.id); + if (!projectShare) { + return res.status(404).send("ProjectShare not found."); + } + + await projectShareService.del(req.params.id); + res.sendStatus(204); + } catch (err) { + res.status(500).send(err); + } +}; + +module.exports = { + getById, + getByProjectId, + post: [ + validate({ body: projectShareSchema }), + post, + validationErrorMiddleware + ], + del +}; diff --git a/server/app/routes/dro.routes.js b/server/app/routes/dro.routes.js new file mode 100644 index 00000000..9f8eba95 --- /dev/null +++ b/server/app/routes/dro.routes.js @@ -0,0 +1,11 @@ +const router = require("express").Router(); +const droController = require("../controllers/dro.controller"); +const jwtSession = require("../../middleware/jwt-session"); + +module.exports = router; + +router.get("/", jwtSession.validateUser, droController.getAll); +router.get("/:id", jwtSession.validateUser, droController.getById); +router.post("/", jwtSession.validateUser, droController.post); +router.put("/:id", jwtSession.validateUser, droController.put); +router.delete("/:id", jwtSession.validateUser, droController.del); diff --git a/server/app/routes/index.js b/server/app/routes/index.js index 7615c638..0b5c2e40 100644 --- a/server/app/routes/index.js +++ b/server/app/routes/index.js @@ -7,6 +7,8 @@ const projectRoutes = require("./project.routes"); const feedbackRoutes = require("./feedback.routes"); const emailRoutes = require("./email.routes"); const configRoutes = require("./config.routes"); +const droRoutes = require("./dro.routes"); +const projectShare = require("./projectShare.routes"); module.exports = router; @@ -17,3 +19,5 @@ router.use("/faqcategories", faqCategoryRoutes); router.use("/feedbacks", feedbackRoutes); router.use("/emails", emailRoutes); router.use("/configs", configRoutes); +router.use("/dro", droRoutes); +router.use("/projectShare", projectShare); diff --git a/server/app/routes/project.routes.js b/server/app/routes/project.routes.js index 3b86ac21..8847543c 100644 --- a/server/app/routes/project.routes.js +++ b/server/app/routes/project.routes.js @@ -15,10 +15,26 @@ router.post("/", jwtSession.validateUser, projectController.post); router.put("/hide", jwtSession.validateUser, projectController.hide); router.put("/trash", jwtSession.validateUser, projectController.trash); router.put("/snapshot", jwtSession.validateUser, projectController.snapshot); +router.put("/submit", jwtSession.validateUser, projectController.submit); router.put( "/renameSnapshot", jwtSession.validateUser, projectController.renameSnapshot ); router.put("/:id", jwtSession.validateUser, projectController.put); +router.put( + "/updateDroId/:id", + jwtSession.validateUser, + projectController.updateDroId +); +router.put( + "/updateAdminNotes/:id", + jwtSession.validateUser, + projectController.updateAdminNotes +); router.delete("/:id", jwtSession.validateUser, projectController.del); +router.put( + "/updateTotals/:id", + jwtSession.validateUser, + projectController.updateTotals +); diff --git a/server/app/routes/projectShare.routes.js b/server/app/routes/projectShare.routes.js new file mode 100644 index 00000000..af645539 --- /dev/null +++ b/server/app/routes/projectShare.routes.js @@ -0,0 +1,14 @@ +const router = require("express").Router(); +const projectShareController = require("../controllers/projectShare.controller"); +const jwtSession = require("../../middleware/jwt-session"); + +module.exports = router; + +router.get("/:id", jwtSession.validateUser, projectShareController.getById); +router.get( + "/projectId/:projectId", + jwtSession.validateUser, + projectShareController.getByProjectId +); +router.post("/", jwtSession.validateUser, projectShareController.post); +router.delete("/:id", jwtSession.validateUser, projectShareController.del); diff --git a/server/app/schemas/dro.js b/server/app/schemas/dro.js new file mode 100644 index 00000000..4a19d23a --- /dev/null +++ b/server/app/schemas/dro.js @@ -0,0 +1,11 @@ +const droSchema = { + type: "object", + properties: { + name: { type: "string", minLength: 1, maxLength: 200 }, + displayOrder: { type: "integer", minimum: 1 } + }, + required: ["name", "displayOrder"], + additionalProperties: false +}; + +module.exports = droSchema; diff --git a/server/app/schemas/project.js b/server/app/schemas/project.js index 48e10e73..98af7962 100644 --- a/server/app/schemas/project.js +++ b/server/app/schemas/project.js @@ -19,6 +19,15 @@ module.exports = { }, calculationId: { type: "number" + }, + targetPoints: { + type: "number" + }, + earnedPoints: { + type: "number" + }, + projectLevel: { + type: "number" } } }; diff --git a/server/app/schemas/projectShare.js b/server/app/schemas/projectShare.js new file mode 100644 index 00000000..1acf019b --- /dev/null +++ b/server/app/schemas/projectShare.js @@ -0,0 +1,14 @@ +module.exports = { + type: "object", + required: ["email", "projectId"], + properties: { + email: { + type: "string", + minLength: 3, + pattern: "\\S+@\\S+" + }, + projectId: { + type: "number" + } + } +}; diff --git a/server/app/services/dro.service.js b/server/app/services/dro.service.js new file mode 100644 index 00000000..85cec049 --- /dev/null +++ b/server/app/services/dro.service.js @@ -0,0 +1,75 @@ +const { pool, poolConnect } = require("./tedious-pool"); +const mssql = require("mssql"); + +const getAll = async () => { + try { + await poolConnect; + const request = pool.request(); + const response = await request.execute("Dro_SelectAll"); + return response.recordset; + } catch (err) { + console.log(err); + } +}; + +const getById = async id => { + try { + await poolConnect; + const request = pool.request(); + request.input("Id", mssql.Int, id); + const response = await request.execute("Dro_SelectById"); + if (response.recordset && response.recordset.length > 0) { + return response.recordset[0]; + } else { + return null; + } + } catch (err) { + return Promise.reject(err); + } +}; + +const post = async item => { + try { + await poolConnect; + const request = pool.request(); + request.input("name", mssql.NVarChar, item.name); // NVARCHAR(200) + request.input("displayOrder", mssql.Int, item.displayOrder); // INT + request.output("id", mssql.Int, null); + const response = await request.execute("Dro_Insert"); + return response.output; + } catch (err) { + return Promise.reject(err); + } +}; + +const put = async item => { + try { + await poolConnect; + const request = pool.request(); + request.input("id", mssql.Int, item.id); + request.input("name", mssql.NVarChar, item.name); // NVARCHAR(200) + request.input("displayOrder", mssql.Int, item.displayOrder); // INT + await request.execute("Dro_Update"); + } catch (err) { + return Promise.reject(err); + } +}; + +const del = async id => { + try { + await poolConnect; + const request = pool.request(); + request.input("id", mssql.Int, id); + await request.execute("Dro_Delete"); + } catch (err) { + return Promise.reject(err); + } +}; + +module.exports = { + getAll, + getById, + post, + put, + del +}; diff --git a/server/app/services/project.service.js b/server/app/services/project.service.js index e2b0d62a..063d1486 100644 --- a/server/app/services/project.service.js +++ b/server/app/services/project.service.js @@ -38,6 +38,9 @@ const post = async item => { request.input("address", mssql.NVarChar, item.address); // 200 request.input("description", mssql.NVarChar, item.description); // max request.input("formInputs", mssql.NVarChar, item.formInputs); // max + request.input("targetPoints", mssql.Int, item.targetPoints); + request.input("earnedPoints", mssql.Int, item.earnedPoints); + request.input("projectLevel", mssql.Int, item.projectLevel); request.input("loginId", mssql.Int, item.loginId); request.input("calculationId", mssql.Int, item.calculationId); request.output("id", mssql.Int, null); @@ -57,6 +60,9 @@ const put = async item => { request.input("address", mssql.NVarChar, item.address); // 200 request.input("description", mssql.NVarChar, item.description); // max request.input("formInputs", mssql.NVarChar, item.formInputs); // max + request.input("targetPoints", mssql.Int, item.targetPoints); + request.input("earnedPoints", mssql.Int, item.earnedPoints); + request.input("projectLevel", mssql.Int, item.projectLevel); request.input("loginId", mssql.Int, item.loginId); request.input("calculationId", mssql.Int, item.calculationId); request.input("id", mssql.Int, item.id); @@ -139,6 +145,22 @@ const snapshot = async (id, loginId, name) => { } }; +const submit = async (id, loginId) => { + try { + await poolConnect; + const request = pool.request(); + + request.input("id", id); + request.input("loginId", loginId); + + const response = await request.execute("Project_Submit"); + return response.returnValue; + } catch (err) { + console.log("err:", err); + return Promise.reject(err); + } +}; + const renameSnapshot = async (id, loginId, name) => { try { await poolConnect; @@ -167,6 +189,78 @@ const getAllArchivedProjects = async () => { } }; +const updateDroId = async (id, droId, loginId) => { + try { + await poolConnect; + const request = pool.request(); + + request.input("id", mssql.Int, id); + if (droId === null) { + request.input("droId", mssql.Int, null); // Correctly pass NULL for droId + } else { + request.input("droId", mssql.Int, droId); // Pass the actual droId value if it's not null + } + request.input( + "DateModifiedAdmin", + mssql.DateTime2, + new Date().toISOString() + ); + request.input("LoginId", mssql.Int, loginId); + + const response = await request.execute("Project_UpdateDroId"); + return response.returnValue; + } catch (err) { + console.log("err:", err); + return Promise.reject(err); + } +}; + +const updateAdminNotes = async (id, adminNotes, loginId) => { + try { + await poolConnect; + const request = pool.request(); + + request.input("id", mssql.Int, id); + request.input("adminNotes", mssql.NVarChar(mssql.MAX), adminNotes); + request.input( + "DateModifiedAdmin", + mssql.DateTime2, + new Date().toISOString() + ); + request.input("LoginId", mssql.Int, loginId); + + const response = await request.execute("Project_UpdateAdminNotes"); + return response.returnValue; + } catch (err) { + console.log("err:", err); + return Promise.reject(err); + } +}; + +const updateTotals = async ( + id, + targetPoints, + earnedPoints, + projectLevel, + loginId +) => { + try { + await poolConnect; + const request = pool.request(); + request.input("id", mssql.Int, id); + request.input("targetPoints", mssql.Int, targetPoints); + request.input("earnedPoints", mssql.Int, earnedPoints); + request.input("projectLevel", mssql.Int, projectLevel); + request.input("LoginId", mssql.Int, loginId); + + const response = await request.execute("Project_UpdateTotals"); + return response.returnValue; + } catch (err) { + console.log("err:", err); + return Promise.reject(err); + } +}; + module.exports = { getAll, getById, @@ -175,7 +269,11 @@ module.exports = { del, hide, trash, + submit, snapshot, + updateDroId, + updateAdminNotes, renameSnapshot, - getAllArchivedProjects + getAllArchivedProjects, + updateTotals }; diff --git a/server/app/services/projectShare.service.js b/server/app/services/projectShare.service.js new file mode 100644 index 00000000..1aa2e15c --- /dev/null +++ b/server/app/services/projectShare.service.js @@ -0,0 +1,62 @@ +const { pool, poolConnect } = require("./tedious-pool"); +const mssql = require("mssql"); + +const getById = async id => { + try { + await poolConnect; + const request = pool.request(); + request.input("id", mssql.Int, id); + const response = await request.execute("ProjectShare_SelectById"); + if (response.recordset && response.recordset.length > 0) { + return response.recordset[0]; + } else { + return null; + } + } catch (err) { + return Promise.reject(err); + } +}; + +const getByProjectId = async projectId => { + try { + await poolConnect; + const request = pool.request(); + request.input("ProjectId", mssql.Int, projectId); + const response = await request.execute("ProjectShare_SelectByProjectId"); + return response.recordset; + } catch (err) { + return Promise.reject(err); + } +}; + +const post = async item => { + try { + await poolConnect; + const request = pool.request(); + request.input("email", mssql.NVarChar, item.email); // 100 + request.input("projectId", mssql.Int, item.projectId); + request.output("id", mssql.Int, null); + const response = await request.execute("ProjectShare_Insert"); + return response.output; + } catch (err) { + return Promise.reject(err); + } +}; + +const del = async id => { + try { + await poolConnect; + const request = pool.request(); + request.input("id", mssql.Int, id); + await request.execute("ProjectShare_Delete"); + } catch (err) { + return Promise.reject(err); + } +}; + +module.exports = { + getById, + getByProjectId, + post, + del +}; diff --git a/server/db/migration/V20240529.1948__add_dateSubmitted_column_1704.sql b/server/db/migration/V20240529.1948__add_dateSubmitted_column_1704.sql index 32285571..ada57c8e 100644 --- a/server/db/migration/V20240529.1948__add_dateSubmitted_column_1704.sql +++ b/server/db/migration/V20240529.1948__add_dateSubmitted_column_1704.sql @@ -1 +1 @@ -ALTER TABLE Project ADD dateSubmitted datetime2(0) NULL; \ No newline at end of file +ALTER TABLE Project ADD dateSubmitted datetime2(0) NULL; diff --git a/server/db/migration/V20240822.1446__add_project_submit-sproc.sql b/server/db/migration/V20240822.1446__add_project_submit-sproc.sql new file mode 100644 index 00000000..4ddbf64c --- /dev/null +++ b/server/db/migration/V20240822.1446__add_project_submit-sproc.sql @@ -0,0 +1,23 @@ +CREATE OR ALTER PROC [dbo].[Project_Submit] + @id int + , @loginId int +AS +BEGIN + DECLARE @rc int + SELECT @rc = count(*) FROM Project p WHERE p.id = @id AND p.loginId = @loginId + IF (@rc = 0) + BEGIN + RETURN 1 /* project is not owned by @loginId - throw error */ + END + + IF EXISTS (SELECT count(*) FROM Project p WHERE p.id = @id and p.dateSnapshotted IS NULL) + BEGIN + RETURN 2 /* project is not a snapshot */ + END + + UPDATE Project SET + dateSubmitted = getutcdate() + WHERE Project.id = @id + +END +GO \ No newline at end of file diff --git a/server/db/migration/V20240919.1358__fix_fix_default_affordable_housing_selection_1854.sql b/server/db/migration/V20240919.1358__fix_fix_default_affordable_housing_selection_1854.sql new file mode 100644 index 00000000..dbf38f0e --- /dev/null +++ b/server/db/migration/V20240919.1358__fix_fix_default_affordable_housing_selection_1854.sql @@ -0,0 +1,7 @@ +UPDATE calculationRule SET +choices = '[{"id": "0", "name": "N/A"}, + {"id": "1", "name": "State Density Bonus"}, + {"id": "2", "name": "TOC Tiers 1-3"}, + {"id": "3", "name": "TOC Tier 4"}, + {"id": "4", "name": "100% Affordable"}]' +where calculationId = 1 and code = 'STRATEGY_AFFORDABLE' \ No newline at end of file diff --git a/server/db/migration/V20240922.1420__add_dro_table_and_project_columns.sql b/server/db/migration/V20240922.1420__add_dro_table_and_project_columns.sql new file mode 100644 index 00000000..ab7d2cd3 --- /dev/null +++ b/server/db/migration/V20240922.1420__add_dro_table_and_project_columns.sql @@ -0,0 +1,21 @@ +-- Create Dro table +CREATE TABLE Dro ( + id INT PRIMARY KEY IDENTITY(1,1), + name NVARCHAR(200) NOT NULL, + displayOrder INT NOT NULL DEFAULT 10 +); +GO + +-- Add new columns to Project table +ALTER TABLE Project +ADD + droId INT NULL, + adminNotes VARCHAR(MAX) NULL, + dateModifiedAdmin DATETIME2(7) NULL; +GO + +-- Add the foreign key constraint to link droId in Project to id in Dro +ALTER TABLE Project +ADD CONSTRAINT FK_Project_Dro FOREIGN KEY (droId) +REFERENCES Dro(id); +GO diff --git a/server/db/migration/V20240922.1421__update_project_selectall.sql b/server/db/migration/V20240922.1421__update_project_selectall.sql new file mode 100644 index 00000000..c11cb0d2 --- /dev/null +++ b/server/db/migration/V20240922.1421__update_project_selectall.sql @@ -0,0 +1,58 @@ +-- Add or update Project_SelectAll stored procedure + +ALTER PROC [dbo].[Project_SelectAll] + @loginId int = null +AS +BEGIN + IF EXISTS(SELECT 1 FROM Login WHERE id = @LoginId AND isAdmin = 1) + BEGIN + -- Admin can see all projects + SELECT + p.id + , p.name + , p.address + , p.formInputs + , p.loginId + , p.calculationId + , p.dateCreated + , p.dateModified + , p.description + , author.firstName + , author.lastName + , p.dateHidden + , p.dateTrashed + , p.dateSnapshotted + , p.dateSubmitted + , p.droId -- New column + , p.adminNotes -- New column + , p.dateModifiedAdmin -- New column + FROM Project p + JOIN Login author ON p.loginId = author.id; + END + ELSE + BEGIN + -- User can only see their own projects + SELECT + p.id + , p.name + , p.address + , p.formInputs + , p.loginId + , p.calculationId + , p.dateCreated + , p.dateModified + , p.description + , author.firstName + , author.lastName + , p.dateHidden + , p.dateTrashed + , p.dateSnapshotted + , p.dateSubmitted + , p.droId -- New column + , p.adminNotes -- New column + , p.dateModifiedAdmin -- New column + FROM Project p + JOIN Login author ON p.loginId = author.id + WHERE author.id = ISNULL(@loginId, author.id); + END +END; diff --git a/server/db/migration/V20240922.1422__update_project_select_by_id.sql b/server/db/migration/V20240922.1422__update_project_select_by_id.sql new file mode 100644 index 00000000..15abf318 --- /dev/null +++ b/server/db/migration/V20240922.1422__update_project_select_by_id.sql @@ -0,0 +1,66 @@ +-- Updating the Project_SelectById stored procedure to include new fields + +CREATE OR ALTER PROC [dbo].[Project_SelectById] + @loginId int = null, + @id int +AS +BEGIN + /* + + EXEC dbo.Project_SelectById @loginId = 37, @id = 2 + + EXEC dbo.Project_SelectById @loginId = 11, @id = 2 + + */ + + IF EXISTS(SELECT 1 FROM Login WHERE id = @LoginId AND isAdmin = 1) + BEGIN + SELECT + p.id, + p.name, + p.address, + p.formInputs, + p.loginId, + p.calculationId, + p.dateCreated, + p.dateModified, + p.description, + l.firstName, + l.lastName, + p.droId, -- New column + p.adminNotes, -- New column + p.dateModifiedAdmin, -- New column + p.dateHidden, + p.dateTrashed, + p.dateSnapshotted, + p.dateSubmitted + FROM Project p + JOIN Login l ON p.loginId = l.id + WHERE p.id = @id; + END + ELSE + BEGIN + SELECT + p.id, + p.name, + p.address, + p.formInputs, + p.loginId, + p.calculationId, + p.dateCreated, + p.dateModified, + p.description, + l.firstName, + l.lastName, + p.droId, -- New column + p.adminNotes, -- New column + p.dateModifiedAdmin, -- New column + p.dateHidden, + p.dateTrashed, + p.dateSnapshotted, + p.dateSubmitted + FROM Project p + JOIN Login l ON p.loginId = l.id + WHERE p.id = @id AND p.loginId = ISNULL(@loginId, p.loginId); + END +END; diff --git a/server/db/migration/V20240922.1423__update_droId_and_dateModifiedAdmin_in_project.sql b/server/db/migration/V20240922.1423__update_droId_and_dateModifiedAdmin_in_project.sql new file mode 100644 index 00000000..bc1e526c --- /dev/null +++ b/server/db/migration/V20240922.1423__update_droId_and_dateModifiedAdmin_in_project.sql @@ -0,0 +1,23 @@ +CREATE OR ALTER PROCEDURE [dbo].[Project_UpdateDroId] + @Id INT, + @DroId INT, + @DateModifiedAdmin DATETIME2, + @LoginId INT +AS +BEGIN + -- Only allow the update if the user is an admin + IF EXISTS (SELECT 1 FROM Login WHERE id = @LoginId AND isAdmin = 1) + BEGIN + -- Update the droId and dateModifiedAdmin without touching dateModified + UPDATE Project + SET + droId = @DroId, + dateModifiedAdmin = @DateModifiedAdmin + WHERE id = @Id; + END + ELSE + BEGIN + -- User is not an admin, raise an error + RAISERROR('Only admins can update the droId.', 16, 1); + END +END; diff --git a/server/db/migration/V20240922.1424__update_admin_notes_and_dateModifiedAdmin_in_project.sql b/server/db/migration/V20240922.1424__update_admin_notes_and_dateModifiedAdmin_in_project.sql new file mode 100644 index 00000000..37a97594 --- /dev/null +++ b/server/db/migration/V20240922.1424__update_admin_notes_and_dateModifiedAdmin_in_project.sql @@ -0,0 +1,24 @@ +CREATE OR ALTER PROCEDURE [dbo].[Project_UpdateAdminNotes] + @Id INT, + @adminNotes NVARCHAR(MAX), + @DateModifiedAdmin DATETIME2, + @LoginId INT + +AS +BEGIN + -- Only allow the update if the user is an admin + IF EXISTS (SELECT 1 FROM Login WHERE id = @LoginId AND isAdmin = 1) + BEGIN + -- Update the project with admin notes and dateModifiedAdmin + UPDATE Project + SET + adminNotes = @adminNotes, + dateModifiedAdmin = GETUTCDATE() + WHERE id = @Id; + END + ELSE + BEGIN + -- User is not an admin, raise an error + RAISERROR('Only admins can update the admin notes.', 16, 1); + END +END; diff --git a/server/db/migration/V20240922.1509__add_dro_selectall.sql b/server/db/migration/V20240922.1509__add_dro_selectall.sql new file mode 100644 index 00000000..c2dcf466 --- /dev/null +++ b/server/db/migration/V20240922.1509__add_dro_selectall.sql @@ -0,0 +1,10 @@ +CREATE PROCEDURE [dbo].[Dro_SelectAll] +AS +BEGIN + SELECT + id, + name, + displayOrder + FROM [dbo].[Dro]; +END; +GO diff --git a/server/db/migration/V20240922.1510__add_dro_select_by_id.sql b/server/db/migration/V20240922.1510__add_dro_select_by_id.sql new file mode 100644 index 00000000..1441a1c0 --- /dev/null +++ b/server/db/migration/V20240922.1510__add_dro_select_by_id.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[Dro_SelectById] + @Id INT +AS +BEGIN + SELECT + id, + name, + displayOrder + FROM [dbo].[Dro] + WHERE id = @Id; +END; +GO diff --git a/server/db/migration/V20240922.1511__add_dro_insert.sql b/server/db/migration/V20240922.1511__add_dro_insert.sql new file mode 100644 index 00000000..02bac0b6 --- /dev/null +++ b/server/db/migration/V20240922.1511__add_dro_insert.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[Dro_Insert] + @name NVARCHAR(200), + @displayOrder INT, + @id INT OUTPUT +AS +BEGIN + INSERT INTO [dbo].[Dro] (name, displayOrder) + VALUES (@name, @displayOrder); + + SET @id = SCOPE_IDENTITY(); +END; +GO diff --git a/server/db/migration/V20240922.1512__add_dro_update.sql b/server/db/migration/V20240922.1512__add_dro_update.sql new file mode 100644 index 00000000..194a3036 --- /dev/null +++ b/server/db/migration/V20240922.1512__add_dro_update.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[Dro_Update] + @id INT, + @name NVARCHAR(200), + @displayOrder INT +AS +BEGIN + UPDATE [dbo].[Dro] + SET + name = @name, + displayOrder = @displayOrder + WHERE id = @id; +END; +GO diff --git a/server/db/migration/V20240922.1513__add_dro_delete.sql b/server/db/migration/V20240922.1513__add_dro_delete.sql new file mode 100644 index 00000000..13da0705 --- /dev/null +++ b/server/db/migration/V20240922.1513__add_dro_delete.sql @@ -0,0 +1,8 @@ +CREATE PROCEDURE [dbo].[Dro_Delete] + @id INT +AS +BEGIN + DELETE FROM [dbo].[Dro] + WHERE id = @id; +END; +GO diff --git a/server/db/migration/V20241002.1928__add_new_dro_entries.sql b/server/db/migration/V20241002.1928__add_new_dro_entries.sql new file mode 100644 index 00000000..0e22e809 --- /dev/null +++ b/server/db/migration/V20241002.1928__add_new_dro_entries.sql @@ -0,0 +1,5 @@ +INSERT INTO dbo.Dro (name, displayOrder) +VALUES +('Metro Development Review', 1), +('Valley Development Review', 2), +('West Los Angeles Development Review', 3); \ No newline at end of file diff --git a/server/db/migration/V20241004.1347__rename_existing_dro.sql b/server/db/migration/V20241004.1347__rename_existing_dro.sql new file mode 100644 index 00000000..0a906f99 --- /dev/null +++ b/server/db/migration/V20241004.1347__rename_existing_dro.sql @@ -0,0 +1,23 @@ +UPDATE dbo.Dro +SET name = 'Metro' +WHERE name = 'Metro Development Review'; + +UPDATE dbo.Dro +SET name = 'Valley' +WHERE name = 'Valley Development Review'; + +UPDATE dbo.Dro +SET name = 'West LA' +WHERE name = 'West Los Angeles Development Review'; + +UPDATE dbo.Dro +SET displayOrder = 1 +WHERE name = 'Metro'; + +UPDATE dbo.Dro +SET displayOrder = 2 +WHERE name = 'Valley'; + +UPDATE dbo.Dro +SET displayOrder = 3 +WHERE name = 'West LA'; \ No newline at end of file diff --git a/server/db/migration/V20241008.1756__add_projectShare_table.sql b/server/db/migration/V20241008.1756__add_projectShare_table.sql new file mode 100644 index 00000000..df3d15b0 --- /dev/null +++ b/server/db/migration/V20241008.1756__add_projectShare_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE ProjectShare ( + id INT PRIMARY KEY IDENTITY(1,1), + email NVARCHAR(100) NOT NULL, + projectId INT NOT NULL FOREIGN KEY REFERENCES Project(id) +); +GO \ No newline at end of file diff --git a/server/db/migration/V20241008.1757__add_projectShare_insert.sql b/server/db/migration/V20241008.1757__add_projectShare_insert.sql new file mode 100644 index 00000000..325218ad --- /dev/null +++ b/server/db/migration/V20241008.1757__add_projectShare_insert.sql @@ -0,0 +1,11 @@ +CREATE PROCEDURE [dbo].[ProjectShare_Insert] + @email NVARCHAR(100), + @projectId INT, + @id INT OUTPUT +AS +BEGIN + INSERT INTO [dbo].[ProjectShare] (email, projectId) + VALUES (@email, @projectId); + SET @id = SCOPE_IDENTITY(); +END; +GO diff --git a/server/db/migration/V20241008.1758__add_projectShare_delete.sql b/server/db/migration/V20241008.1758__add_projectShare_delete.sql new file mode 100644 index 00000000..c65c7629 --- /dev/null +++ b/server/db/migration/V20241008.1758__add_projectShare_delete.sql @@ -0,0 +1,8 @@ +CREATE PROCEDURE [dbo].[ProjectShare_Delete] + @id int +AS +BEGIN + DELETE FROM [dbo].[ProjectShare] + WHERE id = @id; +END; +GO diff --git a/server/db/migration/V20241008.1759__add_projectShare_select_by_projectId.sql b/server/db/migration/V20241008.1759__add_projectShare_select_by_projectId.sql new file mode 100644 index 00000000..0437b50a --- /dev/null +++ b/server/db/migration/V20241008.1759__add_projectShare_select_by_projectId.sql @@ -0,0 +1,14 @@ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE proc [dbo].[ProjectShare_SelectByProjectId] + @projectId INT +AS +BEGIN + SELECT ps.id AS id, ps.email, p.id AS projectid + FROM [dbo].[ProjectShare] ps + INNER JOIN [dbo].[Project] p + ON ps.projectId = p.id and ps.projectId = @projectId; +END +GO \ No newline at end of file diff --git a/server/db/migration/V20241008.1760__add_projectShare_select_by_Id.sql b/server/db/migration/V20241008.1760__add_projectShare_select_by_Id.sql new file mode 100644 index 00000000..8f4facaa --- /dev/null +++ b/server/db/migration/V20241008.1760__add_projectShare_select_by_Id.sql @@ -0,0 +1,9 @@ +CREATE proc [dbo].[ProjectShare_SelectById] + @id INT +AS +BEGIN + SELECT id, email, projectId + FROM [dbo].[ProjectShare] + WHERE id = @id; +END +GO \ No newline at end of file diff --git a/server/db/migration/V20241107.1155__add_cols_to_project_table.sql b/server/db/migration/V20241107.1155__add_cols_to_project_table.sql new file mode 100644 index 00000000..4bcd1e6a --- /dev/null +++ b/server/db/migration/V20241107.1155__add_cols_to_project_table.sql @@ -0,0 +1,219 @@ +ALTER TABLE Project ADD + targetPoints int NULL, + earnedPoints int NULL, + projectLevel int NULL + +GO + +CREATE OR ALTER PROC [dbo].[Project_Insert] + @name nvarchar(200) + , @address nvarchar(200) + , @formInputs nvarchar(max) + , @targetPoints int + , @earnedPoints int + , @projectLevel int + , @loginId int + , @calculationId int + , @description nvarchar(max) + , @id int output +AS +BEGIN + + INSERT Project + ( + name + , address + , formInputs + , targetPoints + , earnedPoints + , projectLevel + , loginId + , calculationId + , description + ) + VALUES + ( + @name + , @address + , @formInputs + , @targetPoints + , @earnedPoints + , @projectLevel + , @loginId + , @calculationId + , @description + ) + + SET @id = SCOPE_IDENTITY() +END +GO + +CREATE OR ALTER PROC [dbo].[Project_Update] + @id int + , @name nvarchar(200) + , @address nvarchar(200) + , @formInputs nvarchar(max) + , @targetPoints int + , @earnedPoints int + , @projectLevel int + , @loginId int + , @calculationId int + , @description nvarchar(max) +AS +BEGIN + + UPDATE Project SET + name = @name + , address = @address + , formInputs = @formInputs + , targetPoints = @targetPoints + , earnedPoints = @earnedPoints + , projectLevel = @projectLevel + , loginId = @loginId + , calculationId = @calculationId + , description = @description + , DateModified = getutcdate() + WHERE + id = @id + +END +GO + + + +CREATE OR ALTER PROC [dbo].[Project_SelectById] + @loginId int = null, + @id int +AS +BEGIN + + IF EXISTS(SELECT 1 FROM Login WHERE id = @LoginId AND isAdmin = 1) + BEGIN + SELECT + p.id, + p.name, + p.address, + p.formInputs, + p.targetPoints, + p.earnedPoints, + p.projectLevel, + p.loginId, + p.calculationId, + p.dateCreated, + p.dateModified, + p.description, + l.firstName, + l.lastName, + p.droId, -- New column + p.adminNotes, -- New column + p.dateModifiedAdmin, -- New column + p.dateHidden, + p.dateTrashed, + p.dateSnapshotted, + p.dateSubmitted + FROM Project p + JOIN Login l ON p.loginId = l.id + WHERE p.id = @id; + END + ELSE + BEGIN + SELECT + p.id, + p.name, + p.address, + p.formInputs, + p.targetPoints, + p.earnedPoints, + p.projectLevel, + p.loginId, + p.calculationId, + p.dateCreated, + p.dateModified, + p.description, + l.firstName, + l.lastName, + p.droId, -- New column + p.adminNotes, -- New column + p.dateModifiedAdmin, -- New column + p.dateHidden, + p.dateTrashed, + p.dateSnapshotted, + p.dateSubmitted + FROM Project p + JOIN Login l ON p.loginId = l.id + WHERE p.id = @id AND p.loginId = ISNULL(@loginId, p.loginId); + END +END; +GO + +CREATE OR ALTER PROC [dbo].[Project_SelectAll] + @loginId int = null +AS +BEGIN + IF EXISTS(SELECT 1 FROM Login WHERE id = @LoginId AND isAdmin = 1) + BEGIN + -- Admin can see all projects + SELECT + p.id + , p.name + , p.address + , p.formInputs + , p.targetPoints + , p.earnedPoints + , p.projectLevel + , p.loginId + , p.calculationId + , p.dateCreated + , p.dateModified + , p.description + , author.firstName + , author.lastName + , p.dateHidden + , p.dateTrashed + , p.dateSnapshotted + , p.dateSubmitted + , p.droId -- New column + , p.adminNotes -- New column + , p.dateModifiedAdmin -- New column + FROM Project p + JOIN Login author ON p.loginId = author.id; + END + ELSE + BEGIN + -- User can only see their own projects + SELECT + p.id + , p.name + , p.address + , p.formInputs + , p.targetPoints + , p.earnedPoints + , p.projectLevel + , p.loginId + , p.calculationId + , p.dateCreated + , p.dateModified + , p.description + , author.firstName + , author.lastName + , p.dateHidden + , p.dateTrashed + , p.dateSnapshotted + , p.dateSubmitted + , p.droId -- New column + , p.adminNotes -- New column + , p.dateModifiedAdmin -- New column + FROM Project p + JOIN Login author ON p.loginId = author.id + WHERE author.id = ISNULL(@loginId, author.id); + END +END; +GO + + + + + + + + diff --git a/server/db/migration/V20241107.1631__fix_project_submit-sproc.sql b/server/db/migration/V20241107.1631__fix_project_submit-sproc.sql new file mode 100644 index 00000000..c9f7297d --- /dev/null +++ b/server/db/migration/V20241107.1631__fix_project_submit-sproc.sql @@ -0,0 +1,25 @@ +CREATE OR ALTER PROC [dbo].[Project_Submit] + @id int + , @loginId int +AS +BEGIN + DECLARE @rc int + SELECT @rc = count(*) FROM Project p WHERE p.id = @id AND p.loginId = @loginId + IF (@rc = 0) + BEGIN + RETURN 1 /* project is not owned by @loginId - throw error */ + END + + IF (SELECT count(*) FROM Project p WHERE p.id = @id and p.dateSnapshotted IS NULL) > 0 + BEGIN + RETURN 2 /* project is not a snapshot */ + END + + UPDATE Project SET + dateSubmitted = getutcdate() + WHERE Project.id = @id + +END +GO + + diff --git a/server/middleware/jwt-session.js b/server/middleware/jwt-session.js index 20e24807..3f836d91 100644 --- a/server/middleware/jwt-session.js +++ b/server/middleware/jwt-session.js @@ -20,11 +20,12 @@ async function login(req, res) { isAdmin: req.user.isAdmin, isSecurityAdmin: req.user.isSecurityAdmin }); + const expirationDateTime = new Date(Date.now() + 43200000); // 12 hours res.cookie("jwt", token, { httpOnly: true, - expires: new Date(Date.now() + 43200000) // 12 hours + expires: expirationDateTime }); - const user = req.user; + const user = { ...req.user, expiration: expirationDateTime }; res.json({ isSuccess: true, token: token, user }); } diff --git a/server/package-lock.json b/server/package-lock.json index 8b332332..dc204d9d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -15,7 +15,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "error-handler": "^1.0.0", - "express": "^4.19.2", + "express": "^4.21.0", "express-json-validator-middleware": "^3.0.1", "express-pino-logger": "^7.0.0", "jsonwebtoken": "^9.0.2", @@ -2758,9 +2758,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -2770,7 +2770,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -2918,19 +2918,25 @@ }, "node_modules/bytes": { "version": "3.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "engines": { "node": ">= 0.8" } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3362,7 +3368,8 @@ }, "node_modules/debug": { "version": "2.6.9", - "license": "MIT", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dependencies": { "ms": "2.0.0" } @@ -3540,16 +3547,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -3574,14 +3584,16 @@ }, "node_modules/depd": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "engines": { "node": ">= 0.8" } }, "node_modules/destroy": { "version": "1.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -3756,7 +3768,8 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { "version": "1.4.581", @@ -3783,8 +3796,9 @@ "dev": true }, "node_modules/encodeurl": { - "version": "1.0.2", - "license": "MIT", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -3809,6 +3823,25 @@ "version": "1.0.0", "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3820,7 +3853,8 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -4110,7 +4144,8 @@ }, "node_modules/etag": { "version": "1.8.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "engines": { "node": ">= 0.6" } @@ -4186,36 +4221,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -4432,11 +4467,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "license": "MIT", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -4525,20 +4561,6 @@ "url": "https://ko-fi.com/tunnckoCore/commissions" } }, - "node_modules/formidable/node_modules/qs": { - "version": "6.11.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/forwarded": { "version": "0.2.0", "license": "MIT", @@ -4548,7 +4570,8 @@ }, "node_modules/fresh": { "version": "0.5.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "engines": { "node": ">= 0.6" } @@ -4665,15 +4688,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4804,20 +4831,20 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -4827,7 +4854,8 @@ }, "node_modules/has-symbols": { "version": "1.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "engines": { "node": ">= 0.4" }, @@ -4867,7 +4895,8 @@ }, "node_modules/http-errors": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -4951,7 +4980,8 @@ }, "node_modules/iconv-lite": { "version": "0.4.24", - "license": "MIT", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -6467,14 +6497,19 @@ }, "node_modules/media-typer": { "version": "0.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "engines": { "node": ">= 0.6" } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "license": "MIT" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -6511,7 +6546,8 @@ }, "node_modules/mime": { "version": "1.6.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "bin": { "mime": "cli.js" }, @@ -6603,7 +6639,8 @@ }, "node_modules/ms": { "version": "2.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/mssql": { "version": "11.0.1", @@ -6852,9 +6889,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6865,7 +6905,8 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dependencies": { "ee-first": "1.1.1" }, @@ -6999,7 +7040,8 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "engines": { "node": ">= 0.8" } @@ -7043,8 +7085,9 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "license": "MIT" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/path-type": { "version": "4.0.0", @@ -7371,11 +7414,11 @@ ] }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -7415,7 +7458,8 @@ }, "node_modules/range-parser": { "version": "1.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "engines": { "node": ">= 0.6" } @@ -7703,8 +7747,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "license": "MIT", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -7724,18 +7769,28 @@ "node": ">= 0.8.0" } }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serve-static": { - "version": "1.15.0", - "license": "MIT", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -7747,14 +7802,16 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7762,7 +7819,8 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -7784,12 +7842,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "license": "MIT", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7945,7 +8008,8 @@ }, "node_modules/statuses": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "engines": { "node": ">= 0.8" } @@ -8141,20 +8205,6 @@ "dev": true, "license": "MIT" }, - "node_modules/superagent/node_modules/qs": { - "version": "6.11.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/supertest": { "version": "6.3.3", "dev": true, @@ -8460,7 +8510,8 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "engines": { "node": ">=0.6" } @@ -8551,7 +8602,8 @@ }, "node_modules/type-is": { "version": "1.6.18", - "license": "MIT", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -8586,7 +8638,8 @@ }, "node_modules/unpipe": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "engines": { "node": ">= 0.8" } diff --git a/server/package.json b/server/package.json index 2ddc5abd..2f17decf 100644 --- a/server/package.json +++ b/server/package.json @@ -30,7 +30,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "error-handler": "^1.0.0", - "express": "^4.19.2", + "express": "^4.21.0", "express-json-validator-middleware": "^3.0.1", "express-pino-logger": "^7.0.0", "jsonwebtoken": "^9.0.2",