From a88c102b2787c6a7eaae4dc13db6afecd7415662 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 27 Feb 2024 16:04:41 -0800 Subject: [PATCH 1/6] Prettier and Package Update Azura Storage Blob and Trivago Prettier Sort Imports Signed-off-by: Allan Carr --- .prettierrc.js | 16 + _reference/reportFiltersAndSorters.md | 14 +- package-lock.json | 748 +++++++++++++++++++++++++- package.json | 2 + 4 files changed, 772 insertions(+), 8 deletions(-) create mode 100644 .prettierrc.js diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..a2bd52a34 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,16 @@ +exports.default = { + printWidth: 120, + useTabs: false, + tabWidth: 2, + trailingComma: "es5", + semi: true, + singleQuote: false, + bracketSpacing: true, + arrowParens: "always", + jsxSingleQuote: false, + bracketSameLine: false, + endOfLine: "lf", + importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], + importOrderSeparation: true, + importOrderSortSpecifiers: true, +}; diff --git a/_reference/reportFiltersAndSorters.md b/_reference/reportFiltersAndSorters.md index 1bd950794..adb2bc1f0 100644 --- a/_reference/reportFiltersAndSorters.md +++ b/_reference/reportFiltersAndSorters.md @@ -3,7 +3,8 @@ This documentation details the schema required for `.filters` files on the report server. It is used to dynamically modify the graphQL query and provide the user more power over their reports. -# Special Notes +## Special Notes + - When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected ## High level Schema Overview @@ -40,9 +41,10 @@ Filters effect the where clause of the graphQL query. They are used to filter th A note on special notation used in the `name` field. ## Reflection + Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file. -``` +```json { "name": "jobs.status", "translation": "jobs.fields.status", @@ -67,7 +69,7 @@ The following cases are available - `special.employees` - This will reflect the employees `bodyshop.employees` - `special.first_names` - This will reflect the first names `bodyshop.employees` - `special.last_names` - This will reflect the last names `bodyshop.employees` -- + ### Path without brackets, multi level `"name": "jobs.joblines.mod_lb_hrs",` @@ -104,6 +106,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! ``` ### Path with brackets,top level + `"name": "[jobs].joblines.mod_lb_hrs",` This will produce a where clause at the `jobs` level of the graphQL query. @@ -138,6 +141,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! ``` ## Known Caveats + - Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not. - The `dates` object is not yet implemented and will be added in a future release. - The type object must be 'string' or 'number' and is case-sensitive. @@ -146,10 +150,12 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! - Do not add the ability to filter on things like FK constraints, must like the above example. ## Sorters + - Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level. - Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters. ### Default Sorters + - A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves. - The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`. @@ -164,4 +170,4 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! "direction": "asc" } } -``` \ No newline at end of file +``` diff --git a/package-lock.json b/package-lock.json index 889051d11..9ce8710ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@aws-sdk/client-secrets-manager": "^3.454.0", "@aws-sdk/client-ses": "^3.454.0", "@aws-sdk/credential-provider-node": "^3.451.0", + "@azure/storage-blob": "^12.17.0", "@opensearch-project/opensearch": "^2.4.0", "aws4": "^1.12.0", "axios": "^1.6.2", @@ -52,6 +53,7 @@ "xmlbuilder2": "^3.1.1" }, "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^4.3.0", "concurrently": "^8.2.2", "source-map-explorer": "^2.5.2" }, @@ -695,11 +697,496 @@ "tslib": "^2.3.1" } }, - "node_modules/@babel/parser": { + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.6.0.tgz", + "integrity": "sha512-3X9wzaaGgRaBCwhLQZDtFp5uLIXCPrGbwJNWPPugvL4xbIGgScv77YzzxToKGLAKvG9amDoofMoP+9hsH1vs1w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz", + "integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.4.tgz", + "integrity": "sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-util": "^1.1.1", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.3", + "form-data": "^4.0.0", + "node-fetch": "^2.6.7", + "process": "^0.11.10", + "tslib": "^2.2.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/core-http/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@azure/core-http/node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@azure/core-http/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.6.0.tgz", + "integrity": "sha512-PyRNcaIOfMgoUC01/24NoG+k8O81VrKxYARnDlo+Q2xji0/0/j2nIt8BwQh294pb1c5QnXTDPbNR4KzoDKXEoQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz", + "integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz", + "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "dependencies": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.7.0.tgz", + "integrity": "sha512-Zq2i3QO6k9DA8vnm29mYM4G8IE9u1mhF1GUabVEqPNX8Lj833gdxQ2NAFxt2BZsfAL+e9cT8SyVN7dFVJ/Hf0g==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz", + "integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", + "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.17.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.17.0.tgz", + "integrity": "sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-http": "^3.0.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", - "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", - "optional": true, + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "devOptional": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -718,6 +1205,97 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -1034,6 +1612,54 @@ "resolved": "https://registry.npmjs.org/@jonkemp/package-utils/-/package-utils-1.0.8.tgz", "integrity": "sha512-bIcKnH5YmtTYr7S6J3J86dn/rFiklwRpOqbTOQ9C0WMmR9FKHVb3bxs2UYfqEmNb93O4nbA97sb6rtz33i9SyA==" }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz", + "integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz", + "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jsdoc/salty": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz", @@ -1106,6 +1732,14 @@ "yarn": "^1.22.10" } }, + "node_modules/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1714,6 +2348,29 @@ "node": ">= 6" } }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", + "integrity": "sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==", + "dev": true, + "dependencies": { + "@babel/generator": "7.17.7", + "@babel/parser": "^7.20.5", + "@babel/traverse": "7.23.2", + "@babel/types": "7.17.0", + "javascript-natural-sort": "0.7.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -1857,6 +2514,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/qs": { "version": "6.9.10", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", @@ -1906,6 +2572,14 @@ "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, + "node_modules/@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -3307,6 +3981,14 @@ "node": ">=6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -3854,6 +4536,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/google-auth-library": { "version": "8.9.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", @@ -4446,6 +5137,12 @@ "node": "*" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, "node_modules/jose": { "version": "4.15.4", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", @@ -4459,6 +5156,12 @@ "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -4540,6 +5243,18 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "optional": true }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-2-csv": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.0.1.tgz", @@ -5457,6 +6172,14 @@ "node": ">= 0.8.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6894,6 +7617,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -6949,6 +7681,14 @@ "node": ">=0.6.x" } }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 3625e8808..4f27f7b17 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@aws-sdk/client-secrets-manager": "^3.454.0", "@aws-sdk/client-ses": "^3.454.0", "@aws-sdk/credential-provider-node": "^3.451.0", + "@azure/storage-blob": "^12.17.0", "@opensearch-project/opensearch": "^2.4.0", "aws4": "^1.12.0", "axios": "^1.6.2", @@ -61,6 +62,7 @@ "xmlbuilder2": "^3.1.1" }, "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^4.3.0", "concurrently": "^8.2.2", "source-map-explorer": "^2.5.2" } From e80e40bb76913ebcaeb5c80f191dc462f883598c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 27 Feb 2024 16:37:07 -0800 Subject: [PATCH 2/6] IO-2654 Local Storage Filter State for Courtesy Car List Signed-off-by: Allan Carr --- .../courtesy-cars-list.component.jsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx index e81a71e3e..3bd49d6e7 100644 --- a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx +++ b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx @@ -17,13 +17,18 @@ import { DateTimeFormatter } from "../../utils/DateFormatter"; import { GenerateDocument } from "../../utils/RenderTemplate"; import { TemplateList } from "../../utils/TemplateConstants"; import { alphaSort } from "../../utils/sorters"; +import useLocalStorage from "../../utils/useLocalStorage"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; + export default function CourtesyCarsList({ loading, courtesycars, refetch }) { const [state, setState] = useState({ sortedInfo: {}, - filteredInfo: { text: "" }, }); const [searchText, setSearchText] = useState(""); + const [filter, setFilter] = useLocalStorage( + "filter_courtesy_cars_list", + null + ); const { t } = useTranslation(); const columns = [ @@ -50,6 +55,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { dataIndex: "status", key: "status", sorter: (a, b) => alphaSort(a.status, b.status), + filteredValue: filter?.status || null, filters: [ { text: t("courtesycars.status.in"), @@ -112,6 +118,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { dataIndex: "readiness", key: "readiness", sorter: (a, b) => alphaSort(a.readiness, b.readiness), + filteredValue: filter?.readiness || null, filters: [ { text: t("courtesycars.readiness.ready"), @@ -227,7 +234,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { ]; const handleTableChange = (pagination, filters, sorter) => { - setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + setState({ ...state, sortedInfo: sorter }); + setFilter(filters); }; const tableData = searchText From a2e0f9fbe7ab8cfd1307e0ab1206a7f3d0c8126f Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 28 Feb 2024 14:01:35 -0800 Subject: [PATCH 3/6] IO-1366 Audit Log for Bill Delete, Job Suspend, Job Void, Correct Saga Signed-off-by: Allan Carr --- .../bill-delete-button.component.jsx | 18 +++++++++++++++++- .../bills-list-table.component.jsx | 4 ++-- .../jobs-detail-header-actions.component.jsx | 10 ++++++++++ .../src/redux/application/application.sagas.js | 2 +- client/src/translations/en_us/common.json | 5 ++++- client/src/translations/es/common.json | 5 ++++- client/src/translations/fr/common.json | 5 ++++- client/src/utils/AuditTrailMappings.js | 4 ++++ 8 files changed, 46 insertions(+), 7 deletions(-) diff --git a/client/src/components/bill-delete-button/bill-delete-button.component.jsx b/client/src/components/bill-delete-button/bill-delete-button.component.jsx index 5d2d154f6..ca3f3822d 100644 --- a/client/src/components/bill-delete-button/bill-delete-button.component.jsx +++ b/client/src/components/bill-delete-button/bill-delete-button.component.jsx @@ -3,10 +3,22 @@ import { useMutation } from "@apollo/client"; import { Button, notification, Popconfirm } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; import { DELETE_BILL } from "../../graphql/bills.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; -export default function BillDeleteButton({ bill, callback }) { +const mapStateToProps = createStructuredSelector({}); +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton); + +export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) { const [loading, setLoading] = useState(false); const { t } = useTranslation(); const [deleteBill] = useMutation(DELETE_BILL); @@ -36,6 +48,10 @@ export default function BillDeleteButton({ bill, callback }) { if (!!!result.errors) { notification["success"]({ message: t("bills.successes.deleted") }); + insertAuditTrail({ + jobid: jobid, + operation: AuditTrailMapping.billdeleted(bill.invoice_number), + }); if (callback && typeof callback === "function") callback(bill.id); } else { diff --git a/client/src/components/bills-list-table/bills-list-table.component.jsx b/client/src/components/bills-list-table/bills-list-table.component.jsx index 9dea48f71..5f5bd7011 100644 --- a/client/src/components/bills-list-table/bills-list-table.component.jsx +++ b/client/src/components/bills-list-table/bills-list-table.component.jsx @@ -9,8 +9,8 @@ import { setModalContext } from "../../redux/modals/modals.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { DateFormatter } from "../../utils/DateFormatter"; -import { alphaSort, dateSort } from "../../utils/sorters"; import { TemplateList } from "../../utils/TemplateConstants"; +import { alphaSort, dateSort } from "../../utils/sorters"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; @@ -58,7 +58,7 @@ export function BillsListTableComponent({ )} - + i18n.t("audit_trail.messages.appointmentinsert", { start }), + billdeleted: (invoice_number) => + i18n.t("audit_trail.messages.billdeleted", { invoice_number }), billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }), billupdated: (invoice_number) => @@ -51,6 +53,8 @@ const AuditTrailMapping = { jobstatuschange: (status) => i18n.t("audit_trail.messages.jobstatuschange", { status }), jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), + jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }), + jobvoid: () => i18n.t("audit_trail.messages.jobvoid"), }; export default AuditTrailMapping; From a45d0bb9f491be7da94b1421b9b7bdccb06325d5 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 29 Feb 2024 14:13:45 -0800 Subject: [PATCH 4/6] IO-1366 Job Exported Audit Trail Signed-off-by: Allan Carr --- .../jobs-close-export-button.component.jsx | 27 ++++++++++++-- .../jobs-export-all-button.component.jsx | 36 ++++++++++++++++--- client/src/pages/dms/dms.container.jsx | 17 +++++++-- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + client/src/utils/AuditTrailMappings.js | 1 + 7 files changed, 76 insertions(+), 8 deletions(-) diff --git a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx index 8bbda03e1..34acb7994 100644 --- a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx +++ b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx @@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect"; import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; import client from "../../utils/GraphQLClient"; const mapStateToProps = createStructuredSelector({ @@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, }); +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + function updateJobCache(items) { client.cache.modify({ id: "ROOT_QUERY", @@ -40,6 +47,7 @@ export function JobsCloseExportButton({ disabled, setSelectedJobs, refetch, + insertAuditTrail, }) { const history = useHistory(); const { t } = useTranslation(); @@ -181,6 +189,10 @@ export function JobsCloseExportButton({ key: "jobsuccessexport", message: t("jobs.successes.exported"), }); + insertAuditTrail({ + jobid: jobId, + operation: AuditTrailMapping.jobexported(), + }); updateJobCache( jobUpdateResponse.data.update_jobs.returning.map((job) => job.id) ); @@ -192,12 +204,20 @@ export function JobsCloseExportButton({ }); } } - if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { + if ( + bodyshop.accountingconfig && + bodyshop.accountingconfig.qbo && + successfulTransactions.length > 0 + ) { notification.open({ type: "success", key: "jobsuccessexport", message: t("jobs.successes.exported"), }); + insertAuditTrail({ + jobid: jobId, + operation: AuditTrailMapping.jobexported(), + }); updateJobCache([ ...new Set( successfulTransactions.map( @@ -227,4 +247,7 @@ export function JobsCloseExportButton({ ); } -export default connect(mapStateToProps, null)(JobsCloseExportButton); +export default connect( + mapStateToProps, + mapDispatchToProps +)(JobsCloseExportButton); diff --git a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx index 3204d7311..2bb338d97 100644 --- a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx +++ b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx @@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect"; import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { UPDATE_JOBS } from "../../graphql/jobs.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; import client from "../../utils/GraphQLClient"; const mapStateToProps = createStructuredSelector({ @@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, }); +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + function updateJobCache(items) { client.cache.modify({ id: "ROOT_QUERY", @@ -41,6 +48,7 @@ export function JobsExportAllButton({ loadingCallback, completedCallback, refetch, + insertAuditTrail, }) { const { t } = useTranslation(); const [updateJob] = useMutation(UPDATE_JOBS); @@ -177,6 +185,12 @@ export function JobsExportAllButton({ key: "jobsuccessexport", message: t("jobs.successes.exported"), }); + jobUpdateResponse.data.update_jobs.returning.forEach((job) => { + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.jobexported(), + }); + }); updateJobCache( jobUpdateResponse.data.update_jobs.returning.map( (job) => job.id @@ -190,13 +204,17 @@ export function JobsExportAllButton({ }); } } - if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { + if ( + bodyshop.accountingconfig && + bodyshop.accountingconfig.qbo && + successfulTransactions.length > 0 + ) { notification.open({ type: "success", key: "jobsuccessexport", message: t("jobs.successes.exported"), }); - updateJobCache([ + const successfulTransactionsSet = [ ...new Set( successfulTransactions.map( (st) => @@ -207,7 +225,14 @@ export function JobsExportAllButton({ ] ) ), - ]); + ]; + if (successfulTransactionsSet.length > 0) { + insertAuditTrail({ + jobid: successfulTransactionsSet[0], + operation: AuditTrailMapping.jobexported(), + }); + } + updateJobCache(successfulTransactionsSet); } } }) @@ -225,4 +250,7 @@ export function JobsExportAllButton({ ); } -export default connect(mapStateToProps, null)(JobsExportAllButton); +export default connect( + mapStateToProps, + mapDispatchToProps +)(JobsExportAllButton); diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx index 77c670e60..9ed0133a9 100644 --- a/client/src/pages/dms/dms.container.jsx +++ b/client/src/pages/dms/dms.container.jsx @@ -26,10 +26,12 @@ import { OwnerNameDisplayFunction } from "../../components/owner-name-display/ow import { auth } from "../../firebase/firebase.utils"; import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries"; import { + insertAuditTrail, setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -38,6 +40,8 @@ const mapStateToProps = createStructuredSelector({ const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), }); export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer); @@ -46,7 +50,7 @@ export const socket = SocketIO( process.env.NODE_ENV === "production" ? process.env.REACT_APP_AXIOS_BASE_API_URL : window.location.origin, - // "http://localhost:4000", // for dev testing, + // "http://localhost:4000", // for dev testing, { path: "/ws", withCredentials: true, @@ -57,7 +61,12 @@ export const socket = SocketIO( } ); -export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { +export function DmsContainer({ + bodyshop, + setBreadcrumbs, + setSelectedHeader, + insertAuditTrail, +}) { const { t } = useTranslation(); const [logLevel, setLogLevel] = useState("DEBUG"); const history = useHistory(); @@ -115,6 +124,10 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { notification.success({ message: t("jobs.successes.exported"), }); + insertAuditTrail({ + jobid: payload, + operation: AuditTrailMapping.jobexported(), + }); history.push("/manage/accounting/receivables"); }); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index c7259897e..9edefeaeb 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -115,6 +115,7 @@ "jobassignmentremoved": "Employee assignment removed for {{operation}}", "jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.", "jobconverted": "Job converted and assigned number {{ro_number}}.", + "jobexported": "Job has been exported.", "jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.", "jobimported": "Job imported.", "jobinproductionchange": "Job production status set to {{inproduction}}", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index ed7cc4efa..2a1e1be9b 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -115,6 +115,7 @@ "jobassignmentremoved": "", "jobchecklist": "", "jobconverted": "", + "jobexported": "", "jobfieldchanged": "", "jobimported": "", "jobinproductionchange": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 638b7d0d6..d2cacffeb 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -115,6 +115,7 @@ "jobassignmentremoved": "", "jobchecklist": "", "jobconverted": "", + "jobexported": "", "jobfieldchanged": "", "jobimported": "", "jobinproductionchange": "", diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js index 757bdd43c..4ad405f0a 100644 --- a/client/src/utils/AuditTrailMappings.js +++ b/client/src/utils/AuditTrailMappings.js @@ -35,6 +35,7 @@ const AuditTrailMapping = { i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }), jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }), + jobexported: () => i18n.t("audit_trail.messages.jobexported"), jobfieldchange: (field, value) => i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), jobimported: () => i18n.t("audit_trail.messages.jobimported"), From a4a84572b71b426183936e3b20170245fb8bd162 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 29 Feb 2024 15:36:58 -0800 Subject: [PATCH 5/6] IO-2656 Job Count on Scoreboard Jobs Signed-off-by: Allan Carr --- .../scoreboard-day-stats.component.jsx | 11 +++-- .../scoreboard-targets-table.component.jsx | 40 +++++++++++++++++-- client/src/translations/en_us/common.json | 3 ++ client/src/translations/es/common.json | 3 ++ client/src/translations/fr/common.json | 3 ++ 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx b/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx index cfd6d945d..ecaf189fb 100644 --- a/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx +++ b/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx @@ -25,6 +25,8 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) { return acc + value.bodyhrs; }, 0); + const numJobs = entries.length; + return ( bodyHrs ? "red" : "green" }} - label="B" + label="Body" value={bodyHrs.toFixed(1)} /> paintHrs ? "red" : "green" }} - label="P" + label="Refinish" value={paintHrs.toFixed(1)} /> - - + + + ); } diff --git a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx index 112d441f6..11129b025 100644 --- a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx +++ b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx @@ -29,10 +29,13 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { let ret = { todayBody: 0, todayPaint: 0, + todayJobs: 0, weeklyPaint: 0, + weeklyJobs: 0, weeklyBody: 0, toDateBody: 0, toDatePaint: 0, + toDateJobs: 0, }; const today = moment(); @@ -40,6 +43,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { dateHash[today.format("YYYY-MM-DD")].forEach((d) => { ret.todayBody = ret.todayBody + d.bodyhrs; ret.todayPaint = ret.todayPaint + d.painthrs; + ret.todayJobs++; }); } @@ -49,6 +53,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => { ret.weeklyBody = ret.weeklyBody + d.bodyhrs; ret.weeklyPaint = ret.weeklyPaint + d.painthrs; + ret.weeklyJobs++; }); } StartOfWeek = StartOfWeek.add(1, "day"); @@ -60,6 +65,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => { ret.toDateBody = ret.toDateBody + d.bodyhrs; ret.toDatePaint = ret.toDatePaint + d.painthrs; + ret.toDateJobs++; }); } startOfMonth = startOfMonth.add(1, "day"); @@ -87,7 +93,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { @@ -140,7 +146,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { @@ -181,7 +187,12 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 501c016ed..8c320540d 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2758,6 +2758,7 @@ "allemployeetimetickets": "All Employee Time Tickets", "asoftodaytarget": "As of Today", "body": "Body", + "bodyabbrev": "B", "bodycharttitle": "Body Targets vs Actual", "calendarperiod": "Periods based on calendar weeks/months.", "combinedcharttitle": "Combined Targets vs Actual", @@ -2774,6 +2775,7 @@ "productivestatistics": "Productive Hours Statistics", "productivetimeticketsoverdate": "Productive Hours over Selected Dates", "refinish": "Refinish", + "refinishabbrev": "R", "refinishcharttitle": "Refinish Targets vs Actual", "targets": "Targets", "thismonth": "This Month", @@ -2781,6 +2783,7 @@ "timetickets": "Time Tickets", "timeticketsemployee": "Time Tickets by Employee", "todateactual": "Actual (MTD)", + "total": "Total", "totalhrs": "Total Hours", "totaloverperiod": "Total over Selected Dates", "weeklyactual": "Actual (W)", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index c9b8d5b63..5152382a9 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2758,6 +2758,7 @@ "allemployeetimetickets": "", "asoftodaytarget": "", "body": "", + "bodyabbrev": "", "bodycharttitle": "", "calendarperiod": "", "combinedcharttitle": "", @@ -2774,6 +2775,7 @@ "productivestatistics": "", "productivetimeticketsoverdate": "", "refinish": "", + "refinishabbrev": "", "refinishcharttitle": "", "targets": "", "thismonth": "", @@ -2781,6 +2783,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "total": "", "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index e2f4705e8..5443f5142 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2758,6 +2758,7 @@ "allemployeetimetickets": "", "asoftodaytarget": "", "body": "", + "bodyabbrev": "", "bodycharttitle": "", "calendarperiod": "", "combinedcharttitle": "", @@ -2774,6 +2775,7 @@ "productivestatistics": "", "productivetimeticketsoverdate": "", "refinish": "", + "refinishabbrev": "", "refinishcharttitle": "", "targets": "", "thismonth": "", @@ -2781,6 +2783,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "total": "", "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", From 0529ac4478a7381d3244637feb4a47f80017519d Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 29 Feb 2024 22:22:55 -0500 Subject: [PATCH 6/6] - Reports V3 Targeted at Master Signed-off-by: Dave Richer --- _reference/reportFiltersAndSorters.md | 31 +- ...center-modal-filters-sorters-component.jsx | 82 +++- .../report-center-modal-utils.js | 28 +- client/src/translations/en_us/common.json | 2 + client/src/translations/es/common.json | 2 + client/src/translations/fr/common.json | 2 + client/src/utils/graphQLmodifier.js | 447 +++++++++++------- 7 files changed, 394 insertions(+), 200 deletions(-) diff --git a/_reference/reportFiltersAndSorters.md b/_reference/reportFiltersAndSorters.md index 1bd950794..8aa65abb9 100644 --- a/_reference/reportFiltersAndSorters.md +++ b/_reference/reportFiltersAndSorters.md @@ -3,6 +3,12 @@ This documentation details the schema required for `.filters` files on the report server. It is used to dynamically modify the graphQL query and provide the user more power over their reports. +For filters and sorters, valid types include (`type` key in the schema): +- string (default) +- number +- bool or boolean +- date + # Special Notes - When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected @@ -67,7 +73,9 @@ The following cases are available - `special.employees` - This will reflect the employees `bodyshop.employees` - `special.first_names` - This will reflect the first names `bodyshop.employees` - `special.last_names` - This will reflect the last names `bodyshop.employees` -- +- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources +- `special.class`- This will reflect the class `bodyshop.md_classes` +- ### Path without brackets, multi level `"name": "jobs.joblines.mod_lb_hrs",` @@ -104,6 +112,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! ``` ### Path with brackets,top level + `"name": "[jobs].joblines.mod_lb_hrs",` This will produce a where clause at the `jobs` level of the graphQL query. @@ -138,16 +147,22 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! ``` ## Known Caveats -- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not. -- The `dates` object is not yet implemented and will be added in a future release. -- The type object must be 'string' or 'number' and is case-sensitive. + +- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` + is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not. +- The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' and is case-sensitive. - The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used. -- Do not add the ability to filter things that are already filtered as part of the original query, this would be redundant and could cause issues. +- Do not add the ability to filter things that are already filtered as part of the original query, this would be + redundant and could cause issues. - Do not add the ability to filter on things like FK constraints, must like the above example. ## Sorters -- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level. -- Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters. + +- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, + a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` + would be added at the `joblines` level. +- Most of the reports currently do sorting on a template level, this will need to change to actually see the results + using the sorters. ### Default Sorters - A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves. @@ -164,4 +179,4 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! "direction": "asc" } } -``` \ No newline at end of file +``` diff --git a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx index 470578ba2..892ce67d5 100644 --- a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx +++ b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx @@ -6,7 +6,7 @@ import {useTranslation} from "react-i18next"; import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import {generateInternalReflections} from "./report-center-modal-utils"; - +import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx"; export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) { return ( @@ -33,7 +33,7 @@ function FiltersSection({filters, form, bodyshop}) { return ( - {(fields, {add, remove, move}) => { + {(fields, {add, remove}) => { return (
{fields.map((field, index) => ( @@ -70,7 +70,9 @@ function FiltersSection({filters, form, bodyshop}) { - + { () => { const name = form.getFieldValue(['filters', field.name, "field"]); @@ -80,7 +82,6 @@ function FiltersSection({filters, form, bodyshop}) { key={`${index}operator`} label={t('reportcenter.labels.advanced_filters_filter_operator')} name={[field.name, "operator"]} - dependencies={[]} rules={[ { required: true, @@ -90,19 +91,32 @@ function FiltersSection({filters, form, bodyshop}) { > trigger.parentNode} + onChange={(value) => { + form.setFieldValue(fieldPath, value); + }} + /> + ); + } return ( trigger.parentNode} + options={[ + { + label: t('reportcenter.labels.advanced_filters_true'), + value: true + }, + { + label: t('reportcenter.labels.advanced_filters_false'), + value: false + } + ]} + onChange={(value) => form.setFieldValue(fieldPath, value)} + /> + ); + } return ( form.setFieldValue(fieldPath, e.target.value)}/> + disabled={!operator} + onChange={(e) => form.setFieldValue(fieldPath, e.target.value)} + /> ); })() } @@ -203,12 +265,12 @@ function FiltersSection({filters, form, bodyshop}) { * @returns {JSX.Element} * @constructor */ -function SortersSection({sorters, form}) { +function SortersSection({sorters}) { const {t} = useTranslation(); return ( - {(fields, {add, remove, move}) => { + {(fields, {add, remove}) => { return (
Sorters diff --git a/client/src/components/report-center-modal/report-center-modal-utils.js b/client/src/components/report-center-modal/report-center-modal-utils.js index 2f9fc5e87..97b693a61 100644 --- a/client/src/components/report-center-modal/report-center-modal-utils.js +++ b/client/src/components/report-center-modal/report-center-modal-utils.js @@ -8,6 +8,21 @@ import {uniqBy} from "lodash"; */ const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj); +/** + * Generate options from array + * @param bodyshop + * @param path + * @returns {unknown[]} + */ +const generateOptionsFromArray = (bodyshop, path) => { + const options = getValueFromPath(bodyshop, path); + return uniqBy(options.map((value) => ({ + label: value, + value: value, + })), 'value'); +} + + /** * Valid internal reflections * Note: This is intended for future functionality @@ -46,15 +61,16 @@ const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => { */ const generateSpecialReflections = (bodyshop, finalPath) => { switch (finalPath) { + // Special case because Referral Sources is an Array, not an Object. + case 'referral_source': + return generateOptionsFromArray(bodyshop, 'md_referral_sources'); + case 'class': + return generateOptionsFromArray(bodyshop, 'md_classes'); case 'cost_centers': return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name'); // Special case because Categories is an Array, not an Object. case 'categories': - const catOptions = getValueFromPath(bodyshop, 'md_categories'); - return uniqBy(catOptions.map((value) => ({ - label: value, - value: value, - })), 'value'); + return generateOptionsFromArray(bodyshop, 'md_categories'); case 'insurance_companies': return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name'); case 'employee_teams': @@ -118,4 +134,4 @@ const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => { } }; -export {generateInternalReflections,} \ No newline at end of file +export {generateInternalReflections} \ No newline at end of file diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 501c016ed..b1f0af354 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2585,6 +2585,8 @@ "advanced_filters_sorters": "Sorters", "advanced_filters_filter_field": "Field", "advanced_filters_sorter_field": "Field", + "advanced_filters_true": "True", + "advanced_filters_false": "False", "advanced_filters_sorter_direction": "Direction", "advanced_filters_filter_operator": "Operator", "advanced_filters_filter_value": "Value", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index c9b8d5b63..9cca30f97 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2585,6 +2585,8 @@ "advanced_filters_sorters": "", "advanced_filters_filter_field": "", "advanced_filters_sorter_field": "", + "advanced_filters_true": "", + "advanced_filters_false": "", "advanced_filters_sorter_direction": "", "advanced_filters_filter_operator": "", "advanced_filters_filter_value": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index e2f4705e8..e234ffac6 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2585,6 +2585,8 @@ "advanced_filters_sorters": "", "advanced_filters_filter_field": "", "advanced_filters_sorter_field": "", + "advanced_filters_true": "", + "advanced_filters_false": "", "advanced_filters_sorter_direction": "", "advanced_filters_filter_operator": "", "advanced_filters_filter_value": "", diff --git a/client/src/utils/graphQLmodifier.js b/client/src/utils/graphQLmodifier.js index 3d4079874..d31c6584a 100644 --- a/client/src/utils/graphQLmodifier.js +++ b/client/src/utils/graphQLmodifier.js @@ -2,22 +2,66 @@ import {Kind, parse, print, visit} from "graphql"; import client from "./GraphQLClient"; import {gql} from "@apollo/client"; +/* eslint-disable no-loop-func */ + +/** + * The available operators for filtering (string) + * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]} + */ const STRING_OPERATORS = [ {value: "_eq", label: "equals"}, {value: "_neq", label: "does not equal"}, {value: "_like", label: "contains"}, {value: "_nlike", label: "does not contain"}, {value: "_ilike", label: "contains case-insensitive"}, - {value: "_nilike", label: "does not contain case-insensitive"} + {value: "_nilike", label: "does not contain case-insensitive"}, + {value: "_in", label: "in", type: "array"}, + {value: "_nin", label: "not in", type: "array"} ]; + +/** + * The available operators for filtering (dates) + * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]} + */ +const DATE_OPERATORS = [ + {value: "_eq", label: "equals"}, + {value: "_neq", label: "does not equal"}, + {value: "_gt", label: "greater than"}, + {value: "_lt", label: "less than"}, + {value: "_gte", label: "greater than or equal"}, + {value: "_lte", label: "less than or equal"}, + {value: "_in", label: "in", type: "array"}, + {value: "_nin", label: "not in", type: "array"} +]; + +/** + * The available operators for filtering (booleans) + * @type {[{label: string, value: string},{label: string, value: string}]} + */ +const BOOLEAN_OPERATORS = [ + {value: "_eq", label: "equals"}, + {value: "_neq", label: "does not equal"}, +]; + +/** + * The available operators for filtering (numbers) + * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]} + */ const NUMBER_OPERATORS = [ {value: "_eq", label: "equals"}, {value: "_neq", label: "does not equal"}, {value: "_gt", label: "greater than"}, {value: "_lt", label: "less than"}, {value: "_gte", label: "greater than or equal"}, - {value: "_lte", label: "less than or equal"} + {value: "_lte", label: "less than or equal"}, + {value: "_in", label: "in", type: "array"}, + {value: "_nin", label: "not in", type: "array"} ]; + +/** + * The available operators for sorting + * @type {[{label: string, value: string},{label: string, value: string}]} + */ const ORDER_BY_OPERATORS = [ {value: "asc", label: "ascending"}, {value: "desc", label: "descending"} @@ -31,7 +75,6 @@ export function getOrderOperatorsByType() { return ORDER_BY_OPERATORS; } - /** * Get the available operators for filtering * @param type @@ -40,13 +83,14 @@ export function getOrderOperatorsByType() { export function getWhereOperatorsByType(type = 'string') { const operators = { string: STRING_OPERATORS, - number: NUMBER_OPERATORS + number: NUMBER_OPERATORS, + boolean: BOOLEAN_OPERATORS, + bool: BOOLEAN_OPERATORS, + date: DATE_OPERATORS }; return operators[type]; } -/* eslint-disable no-loop-func */ - /** * Parse a GraphQL query into an AST * @param query @@ -78,11 +122,9 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u // Parse the query and apply the filters and sorters const ast = parseQuery(templateQueryToExecute); - let filterFields = []; if (templateObject?.filters && templateObject?.filters?.length) { - applyFilters(ast, templateObject.filters, filterFields); - wrapFiltersInAnd(ast, filterFields); + applyFilters(ast, templateObject.filters); } if (templateObject?.sorters && templateObject?.sorters?.length) { @@ -109,7 +151,6 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u return {contextData, useShopSpecificTemplate}; } - /** * Apply sorters to the AST * @param ast @@ -149,16 +190,16 @@ export function applySorters(ast, sorters) { if (!orderByArg) { orderByArg = { kind: Kind.ARGUMENT, - name: { kind: Kind.NAME, value: 'order_by' }, - value: { kind: Kind.OBJECT, fields: [] }, + name: {kind: Kind.NAME, value: 'order_by'}, + value: {kind: Kind.OBJECT, fields: []}, }; currentSelection.arguments.push(orderByArg); } const sorterField = { kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: targetFieldName }, - value: { kind: Kind.ENUM, value: sorter.direction }, // Adjust if your schema uses a different type for sorting directions + name: {kind: Kind.NAME, value: targetFieldName}, + value: {kind: Kind.ENUM, value: sorter.direction}, // Adjust if your schema uses a different type for sorting directions }; // Add the new sorter condition @@ -170,10 +211,59 @@ export function applySorters(ast, sorters) { }); } +/** + * Apply Top Level Sub to the AST + * @param node + * @param fieldPath + * @param filterField + */ +function applyTopLevelSub(node, fieldPath, filterField) { + // Find or create the where argument for the top-level subfield + let whereArg = node.selectionSet.selections + .find(selection => selection.name.value === fieldPath[0]) + ?.arguments.find(arg => arg.name.value === 'where'); + + if (!whereArg) { + whereArg = { + kind: Kind.ARGUMENT, + name: {kind: Kind.NAME, value: 'where'}, + value: {kind: Kind.OBJECT, fields: []}, + }; + const topLevelSubSelection = node.selectionSet.selections.find(selection => + selection.name.value === fieldPath[0] + ); + if (topLevelSubSelection) { + topLevelSubSelection.arguments = topLevelSubSelection.arguments || []; + topLevelSubSelection.arguments.push(whereArg); + } + } + + // Correctly position the nested filter without an extra 'where' + if (fieldPath.length > 2) { // More than one level deep + let currentField = whereArg.value; + fieldPath.slice(1, -1).forEach((path, index) => { + let existingField = currentField.fields.find(f => f.name.value === path); + if (!existingField) { + existingField = { + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: path}, + value: {kind: Kind.OBJECT, fields: []} + }; + currentField.fields.push(existingField); + } + currentField = existingField.value; + }); + currentField.fields.push(filterField); + } else { // Directly under the top level + whereArg.value.fields.push(filterField); + } +} + /** * Apply filters to the AST * @param ast * @param filters + * @returns {ASTNode} */ export function applyFilters(ast, filters) { return visit(ast, { @@ -182,192 +272,197 @@ export function applyFilters(ast, filters) { filters.forEach(filter => { const fieldPath = filter.field.split('.'); let topLevel = false; + let topLevelSub = false; // Determine if the filter should be applied at the top level - if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { - fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets + if (fieldPath.length === 2) { topLevel = true; } - if (topLevel) { - // Construct the filter for a top-level application - const targetFieldName = fieldPath[fieldPath.length - 1]; - const filterValue = { - kind: getGraphQLKind(filter.value), - value: filter.value, - }; - - const nestedFilter = { - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: targetFieldName }, - value: { - kind: Kind.OBJECT, - fields: [{ - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: filter.operator }, - value: filterValue, - }], - }, - }; - - // Find or create the where argument for the top-level field - let whereArg = node.selectionSet.selections - .find(selection => selection.name.value === fieldPath[0]) - ?.arguments.find(arg => arg.name.value === 'where'); - - if (!whereArg) { - whereArg = { - kind: Kind.ARGUMENT, - name: { kind: Kind.NAME, value: 'where' }, - value: { kind: Kind.OBJECT, fields: [] }, - }; - const topLevelSelection = node.selectionSet.selections.find(selection => - selection.name.value === fieldPath[0] - ); - if (topLevelSelection) { - topLevelSelection.arguments = topLevelSelection.arguments || []; - topLevelSelection.arguments.push(whereArg); - } - } - - // Correctly position the nested filter without an extra 'where' - if (fieldPath.length > 2) { // More than one level deep - let currentField = whereArg.value; - fieldPath.slice(1, -1).forEach((path, index) => { - let existingField = currentField.fields.find(f => f.name.value === path); - if (!existingField) { - existingField = { - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: path }, - value: { kind: Kind.OBJECT, fields: [] } - }; - currentField.fields.push(existingField); - } - currentField = existingField.value; - }); - currentField.fields.push(nestedFilter); - } else { // Directly under the top level - whereArg.value.fields.push(nestedFilter); - } - } else { - // Initialize a reference to the current selection to traverse down the AST - let currentSelection = node; - let whereArgFound = false; - - // Iterate over the fieldPath, except for the last entry, to navigate the structure - for (let i = 0; i < fieldPath.length - 1; i++) { - const fieldName = fieldPath[i]; - let fieldFound = false; - - // Check if the current selection has a selectionSet and selections - if (currentSelection.selectionSet && currentSelection.selectionSet.selections) { - // Look for the field in the current selection's selections - const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName); - if (selection) { - // Move down the AST to the found selection - currentSelection = selection; - fieldFound = true; - } - } - - // If the field was not found in the current path, it's an issue - if (!fieldFound) { - console.error(`Field ${fieldName} not found in the current selection.`); - return; // Exit the loop and function due to error - } - } - - // At this point, currentSelection should be the parent field where the filter needs to be applied - // Check if the 'where' argument already exists in the current selection - const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where'); - if (whereArg) { - whereArgFound = true; - } else { - // If not found, create a new 'where' argument for the current selection - currentSelection.arguments.push({ - kind: Kind.ARGUMENT, - name: { kind: Kind.NAME, value: 'where' }, - value: { kind: Kind.OBJECT, fields: [] } // Empty fields array to be populated with the filter - }); - } - - // Assuming the last entry in fieldPath is the field to apply the filter on - const targetField = fieldPath[fieldPath.length - 1]; - const filterValue = { - kind: getGraphQLKind(filter.value), - value: filter.value, - }; - - // Construct the filter field object - const filterField = { - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: targetField }, - value: { - kind: Kind.OBJECT, - fields: [{ - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: filter.operator }, - value: filterValue, - }], - }, - }; - - // Add the filter field to the 'where' clause of the current selection - if (whereArgFound) { - whereArg.value.fields.push(filterField); - } else { - // If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter - currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField); - } + if (fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { + fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets + topLevelSub = true; } + // Construct the filter for a top-level application + const targetFieldName = fieldPath[fieldPath.length - 1]; + + let filterValue = createFilterValue(filter); + let filterField = createFilterField(targetFieldName, filter, filterValue); + + if (topLevel) { + applyTopLevelFilter(node, fieldPath, filterField); + } else if (topLevelSub) { + applyTopLevelSub(node, fieldPath, filterField); + } else { + applyNestedFilter(node, fieldPath, filterField); + } }); } } }); } +/** + * Create a filter value based on the filter + * @param filter + * @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}} + */ +function createFilterValue(filter) { + if (Array.isArray(filter.value)) { + // If it's an array, create a list value with the array items + return { + kind: Kind.LIST, + values: filter.value.map(item => ({ + kind: getGraphQLKind(item), + value: item, + })), + }; + } else { + // If it's not an array, use the existing logic + return { + kind: getGraphQLKind(filter.value), + value: filter.value, + }; + } +} + +/** + * Create a filter field based on the target field and filter + * @param targetFieldName + * @param filter + * @param filterValue + * @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}} + */ +function createFilterField(targetFieldName, filter, filterValue) { + return { + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: targetFieldName}, + value: { + kind: Kind.OBJECT, + fields: [{ + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: filter.operator}, + value: filterValue, + }], + }, + }; +} + +/** + * Apply a top-level filter to the AST + * @param node + * @param fieldPath + * @param filterField + */ +function applyTopLevelFilter(node, fieldPath, filterField) { + // Find or create the where argument for the top-level field + let whereArg = node.selectionSet.selections + .find(selection => selection.name.value === fieldPath[0]) + ?.arguments.find(arg => arg.name.value === 'where'); + + if (!whereArg) { + whereArg = { + kind: Kind.ARGUMENT, + name: {kind: Kind.NAME, value: 'where'}, + value: {kind: Kind.OBJECT, fields: []}, + }; + const topLevelSelection = node.selectionSet.selections.find(selection => + selection.name.value === fieldPath[0] + ); + if (topLevelSelection) { + topLevelSelection.arguments = topLevelSelection.arguments || []; + topLevelSelection.arguments.push(whereArg); + } + } + + // Correctly position the nested filter without an extra 'where' + if (fieldPath.length > 2) { // More than one level deep + let currentField = whereArg.value; + fieldPath.slice(1, -1).forEach((path, index) => { + let existingField = currentField.fields.find(f => f.name.value === path); + if (!existingField) { + existingField = { + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: path}, + value: {kind: Kind.OBJECT, fields: []} + }; + currentField.fields.push(existingField); + } + currentField = existingField.value; + }); + currentField.fields.push(filterField); + } else { // Directly under the top level + whereArg.value.fields.push(filterField); + } +} + +/** + * Apply a nested filter to the AST + * @param node + * @param fieldPath + * @param filterField + */ +function applyNestedFilter(node, fieldPath, filterField) { + // Initialize a reference to the current selection to traverse down the AST + let currentSelection = node; + + // Iterate over the fieldPath, except for the last entry, to navigate the structure + for (let i = 0; i < fieldPath.length - 1; i++) { + const fieldName = fieldPath[i]; + let fieldFound = false; + + // Check if the current selection has a selectionSet and selections + if (currentSelection.selectionSet && currentSelection.selectionSet.selections) { + // Look for the field in the current selection's selections + const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName); + if (selection) { + // Move down the AST to the found selection + currentSelection = selection; + fieldFound = true; + } + } + + // If the field was not found in the current path, it's an issue + if (!fieldFound) { + console.error(`Field ${fieldName} not found in the current selection.`); + return; // Exit the loop and function due to error + } + } + + // At this point, currentSelection should be the parent field where the filter needs to be applied + // Check if the 'where' argument already exists in the current selection + const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where'); + if (!whereArg) { + // If not found, create a new 'where' argument for the current selection + currentSelection.arguments.push({ + kind: Kind.ARGUMENT, + name: {kind: Kind.NAME, value: 'where'}, + value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter + }); + } + + // Add the filter field to the 'where' clause of the current selection + currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField); +} + /** * Get the GraphQL kind for a value * @param value * @returns {Kind|Kind.INT} */ function getGraphQLKind(value) { - if (typeof value === 'number') { + if (Array.isArray(value)) { + return Kind.LIST; + } else if (typeof value === 'number') { return value % 1 === 0 ? Kind.INT : Kind.FLOAT; } else if (typeof value === 'boolean') { return Kind.BOOLEAN; } else if (typeof value === 'string') { return Kind.STRING; + } else if (value instanceof Date) { + return Kind.STRING; // GraphQL does not have a Date type, so we return it as a string } - // Extend with more types as needed } -/** - * Wrap filters in an 'and' object - * @param ast - * @param filterFields - */ -export function wrapFiltersInAnd(ast, filterFields) { - visit(ast, { - OperationDefinition: { - enter(node) { - node.selectionSet.selections.forEach((selection) => { - let whereArg = selection.arguments.find(arg => arg.name.value === 'where'); - if (filterFields.length > 1) { - const andFilter = { - kind: Kind.OBJECT_FIELD, - name: {kind: Kind.NAME, value: '_and'}, - value: {kind: Kind.LIST, values: filterFields} - }; - whereArg.value.fields.push(andFilter); - } else if (filterFields.length === 1) { - whereArg.value.fields.push(filterFields[0].fields[0]); - } - }); - } - } - }); -} - -/* eslint-enable no-loop-func */ \ No newline at end of file +/* eslint-enable no-loop-func */