From c7c5c3d8f532bd1528c7e22d4eeaab1187962398 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Mon, 12 Apr 2021 22:27:11 -0400 Subject: [PATCH 01/17] Add initial server linting --- server/gulpfile.js | 33 +- server/package-lock.json | 1727 +++++++++++------ server/package.json | 24 +- server/src/api/api.graphql | 322 ++- server/src/api/api.ts | 1569 ++++++++------- server/src/api/items/ItemController.ts | 94 +- server/src/api/requests.ts | 179 +- server/src/api/requests/quantity.ts | 263 ++- server/src/app.ts | 202 +- server/src/auth/auth.ts | 180 +- server/src/auth/strategies.ts | 368 ++-- server/src/common.ts | 362 ++-- server/src/database.ts | 87 +- server/src/knexfile.ts | 37 +- .../20190906232528_create_users_table.ts | 24 +- .../20190906235036_create_categories_table.ts | 12 +- .../20190906235544_create_items_table.ts | 37 +- .../20190906235755_create_requests_table.ts | 53 +- .../20191016191258_create_locations_table.ts | 13 +- .../20191019160236_add_location_items.ts | 43 +- .../20191023033514_create_settings_table.ts | 11 +- server/tsconfig.json | 29 +- 22 files changed, 3215 insertions(+), 2454 deletions(-) diff --git a/server/gulpfile.js b/server/gulpfile.js index cc97075..89f3c4f 100644 --- a/server/gulpfile.js +++ b/server/gulpfile.js @@ -1,8 +1,7 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ const gulp = require("gulp"); const gutil = require("gulp-util"); -const tslint = require("gulp-tslint"); const ts = require("gulp-typescript"); -const webpack = require("webpack"); const nodemon = require("nodemon"); const path = require("path"); const gulpCopy = require("gulp-copy"); @@ -10,33 +9,19 @@ const gulpCopy = require("gulp-copy"); const tsProject = ts.createProject("./tsconfig.json"); gulp.task("watch", (done) => { - gulp.watch("src/**/*", gulp.series("lint", "build:server", "build:static")); + gulp.watch("src/**/*", gulp.series("build:server", "build:static")); }); -gulp.task("lint", () => { - return gulp.src(["src/**/*.ts"]) - .pipe(tslint({ - formatter: "prose", - })) - .pipe(tslint.report({ - emitError: false, - })); -}); - -gulp.task("build:static", () => { - return gulp.src(["./src/config/*", "./package.json", "./src/api/api.graphql"]) - .pipe(gulpCopy("build", { "prefix": 1 })); -}); +gulp.task("build:static", () => gulp.src(["./src/config/*", "./package.json", "./src/api/api.graphql"]) + .pipe(gulpCopy("build", { "prefix": 1 }))); -gulp.task("build:server", () => { - return tsProject.src() +gulp.task("build:server", () => tsProject.src() .pipe(tsProject()) - .pipe(gulp.dest("build/")); -}); + .pipe(gulp.dest("build/"))); -gulp.task("build", gulp.series("lint", "build:server", "build:static")); +gulp.task("build", gulp.series("build:server", "build:static")); -gulp.task("serve", gulp.series("lint", "build:server", "build:static", () => { +gulp.task("serve", gulp.series("build:server", "build:static", () => { nodemon({ script: path.join(__dirname, "build/app.js"), watch: ["build/"], @@ -44,7 +29,7 @@ gulp.task("serve", gulp.series("lint", "build:server", "build:static", () => { env: { "NODE_ENV": "dev", }, - }).on("start", function() { + }).on("start", () => { gutil.log(gutil.colors.blue("Server started!")); }); })); diff --git a/server/package-lock.json b/server/package-lock.json index 0717fe6..10e2e79 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -792,6 +792,67 @@ "tslib": "^2" } }, + "@eslint/eslintrc": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz", + "integrity": "sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, "@graphql-codegen/cli": { "version": "1.21.3", "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-1.21.3.tgz", @@ -1672,6 +1733,31 @@ "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", "integrity": "sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg==" }, + "@hex-labs/eslint-config": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@hex-labs/eslint-config/-/eslint-config-1.1.1.tgz", + "integrity": "sha512-7Xv2KJrE3mY7ErJwUGd/9/QGXWpuTQSqCsaFi6Y/MnDonBSMm8bF5GNXLpkH1tzsHL2hnfQKXy38LHxLenz8dQ==", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "^4.9.0", + "@typescript-eslint/parser": "^4.13.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-config-prettier": "^7.1.0", + "eslint-plugin-import": "^2.22.1" + } + }, + "@hex-labs/prettier-config": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@hex-labs/prettier-config/-/prettier-config-1.1.1.tgz", + "integrity": "sha512-GuF+VTq1THJa4tAgJUphMXT2Knp9rGOb3VIBMLQtEgiBS03S5HDS8tmRgcfoIxTKAOQWXYX8sxlFlHnVYgTtLg==", + "dev": true + }, + "@hex-labs/tsconfig": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@hex-labs/tsconfig/-/tsconfig-1.1.1.tgz", + "integrity": "sha512-Pt6kzHfemngKS21B3JcFBz7KWCrnfKBU8w+AT4mmfVLLYmPyJnclyqjeyaHI84g+A0GCruqPvZ4jOG/XmZqFpQ==", + "dev": true + }, "@iarna/toml": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", @@ -1792,29 +1878,6 @@ "integrity": "sha512-0jpd4oizLaBybWBCew/lx4iAjjNWmCfzbspvnjoppyGEquN51BjShmPCksiH2IXLwtxQ2F/e2PkFy0LL7jpLsw==", "dev": true }, - "@types/eslint": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.9.tgz", - "integrity": "sha512-SdAAXZNvWfhtf3X3y1cbbCZhP3xyPh7mfTvzV6CgfWc/ZhiHpyr9bVroe2/RCHIf7gczaNcprhaBLsx0CCJHQA==", - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "@types/eslint-scope": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.0.tgz", - "integrity": "sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==", - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "@types/estree": { - "version": "0.0.46", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz", - "integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==" - }, "@types/express": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.11.tgz", @@ -1868,11 +1931,6 @@ "@types/express": "*" } }, - "@types/fancy-log": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@types/fancy-log/-/fancy-log-1.3.0.tgz", - "integrity": "sha512-mQjDxyOM1Cpocd+vm1kZBP7smwKZ4TNokFeds9LV7OZibmPJFEzY3+xZMrKfUdNT71lv8GoCPD6upKwHxubClw==" - }, "@types/http-proxy-agent": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/http-proxy-agent/-/http-proxy-agent-2.0.2.tgz", @@ -1882,6 +1940,12 @@ "@types/node": "*" } }, + "@types/isomorphic-fetch": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.35.tgz", + "integrity": "sha512-DaZNUvLDCAnCTjgwxgiL1eQdxIKEpNLOlTNtAgnZc50bG2copGhRrFN9/PxPBuJe+tZVLCbQ7ls0xveXVRPkvw==", + "dev": true + }, "@types/js-yaml": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.0.tgz", @@ -1891,7 +1955,8 @@ "@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", - "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", + "dev": true }, "@types/json-stable-stringify": { "version": "1.0.32", @@ -1899,6 +1964,12 @@ "integrity": "sha512-q9Q6+eUEGwQkv4Sbst3J4PNgDOvpuVuKj79Hl/qnmBMEIPzB5QoFRUtjcgcg2xNUZyYUGXBk5wYIBKHt0A+Mxw==", "dev": true }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/jsonwebtoken": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", @@ -2022,141 +2093,192 @@ "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.2.tgz", "integrity": "sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg==" }, - "@ungap/global-this": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@ungap/global-this/-/global-this-0.4.4.tgz", - "integrity": "sha512-mHkm6FvepJECMNthFuIgpAEFmPOk71UyXuIxYfjytvFTnSDBIz7jmViO+LfHI/AjrazWije0PnSP3+/NlwzqtA==" - }, - "@webassemblyjs/ast": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz", - "integrity": "sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg==", - "requires": { - "@webassemblyjs/helper-numbers": "1.11.0", - "@webassemblyjs/helper-wasm-bytecode": "1.11.0" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz", - "integrity": "sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA==" - }, - "@webassemblyjs/helper-api-error": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz", - "integrity": "sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w==" - }, - "@webassemblyjs/helper-buffer": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz", - "integrity": "sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA==" - }, - "@webassemblyjs/helper-numbers": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz", - "integrity": "sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ==", - "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.0", - "@webassemblyjs/helper-api-error": "1.11.0", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz", - "integrity": "sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==" - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz", - "integrity": "sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew==", + "@typescript-eslint/eslint-plugin": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.0.tgz", + "integrity": "sha512-U8SP9VOs275iDXaL08Ln1Fa/wLXfj5aTr/1c0t0j6CdbOnxh+TruXu1p4I0NAvdPBQgoPjHsgKn28mOi0FzfoA==", + "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.0", - "@webassemblyjs/helper-buffer": "1.11.0", - "@webassemblyjs/helper-wasm-bytecode": "1.11.0", - "@webassemblyjs/wasm-gen": "1.11.0" + "@typescript-eslint/experimental-utils": "4.22.0", + "@typescript-eslint/scope-manager": "4.22.0", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "lodash": "^4.17.15", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } } }, - "@webassemblyjs/ieee754": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz", - "integrity": "sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA==", + "@typescript-eslint/experimental-utils": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.0.tgz", + "integrity": "sha512-xJXHHl6TuAxB5AWiVrGhvbGL8/hbiCQ8FiWwObO3r0fnvBdrbWEDy1hlvGQOAWc6qsCWuWMKdVWlLAEMpxnddg==", + "dev": true, "requires": { - "@xtuc/ieee754": "^1.2.0" + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.22.0", + "@typescript-eslint/types": "4.22.0", + "@typescript-eslint/typescript-estree": "4.22.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" } }, - "@webassemblyjs/leb128": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.0.tgz", - "integrity": "sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g==", + "@typescript-eslint/parser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.22.0.tgz", + "integrity": "sha512-z/bGdBJJZJN76nvAY9DkJANYgK3nlRstRRi74WHm3jjgf2I8AglrSY+6l7ogxOmn55YJ6oKZCLLy+6PW70z15Q==", + "dev": true, "requires": { - "@xtuc/long": "4.2.2" + "@typescript-eslint/scope-manager": "4.22.0", + "@typescript-eslint/types": "4.22.0", + "@typescript-eslint/typescript-estree": "4.22.0", + "debug": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } } }, - "@webassemblyjs/utf8": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.0.tgz", - "integrity": "sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw==" - }, - "@webassemblyjs/wasm-edit": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz", - "integrity": "sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ==", + "@typescript-eslint/scope-manager": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.0.tgz", + "integrity": "sha512-OcCO7LTdk6ukawUM40wo61WdeoA7NM/zaoq1/2cs13M7GyiF+T4rxuA4xM+6LeHWjWbss7hkGXjFDRcKD4O04Q==", + "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.0", - "@webassemblyjs/helper-buffer": "1.11.0", - "@webassemblyjs/helper-wasm-bytecode": "1.11.0", - "@webassemblyjs/helper-wasm-section": "1.11.0", - "@webassemblyjs/wasm-gen": "1.11.0", - "@webassemblyjs/wasm-opt": "1.11.0", - "@webassemblyjs/wasm-parser": "1.11.0", - "@webassemblyjs/wast-printer": "1.11.0" + "@typescript-eslint/types": "4.22.0", + "@typescript-eslint/visitor-keys": "4.22.0" } }, - "@webassemblyjs/wasm-gen": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz", - "integrity": "sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ==", - "requires": { - "@webassemblyjs/ast": "1.11.0", - "@webassemblyjs/helper-wasm-bytecode": "1.11.0", - "@webassemblyjs/ieee754": "1.11.0", - "@webassemblyjs/leb128": "1.11.0", - "@webassemblyjs/utf8": "1.11.0" - } + "@typescript-eslint/types": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.0.tgz", + "integrity": "sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA==", + "dev": true }, - "@webassemblyjs/wasm-opt": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz", - "integrity": "sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg==", + "@typescript-eslint/typescript-estree": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.0.tgz", + "integrity": "sha512-TkIFeu5JEeSs5ze/4NID+PIcVjgoU3cUQUIZnH3Sb1cEn1lBo7StSV5bwPuJQuoxKXlzAObjYTilOEKRuhR5yg==", + "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.0", - "@webassemblyjs/helper-buffer": "1.11.0", - "@webassemblyjs/wasm-gen": "1.11.0", - "@webassemblyjs/wasm-parser": "1.11.0" + "@typescript-eslint/types": "4.22.0", + "@typescript-eslint/visitor-keys": "4.22.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } } }, - "@webassemblyjs/wasm-parser": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz", - "integrity": "sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw==", + "@typescript-eslint/visitor-keys": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.0.tgz", + "integrity": "sha512-nnMu4F+s4o0sll6cBSsTeVsT4cwxB7zECK3dFxzEjPBii9xLpq4yqqsy/FU5zMfan6G60DKZSCXAa3sHJZrcYw==", + "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.0", - "@webassemblyjs/helper-api-error": "1.11.0", - "@webassemblyjs/helper-wasm-bytecode": "1.11.0", - "@webassemblyjs/ieee754": "1.11.0", - "@webassemblyjs/leb128": "1.11.0", - "@webassemblyjs/utf8": "1.11.0" + "@typescript-eslint/types": "4.22.0", + "eslint-visitor-keys": "^2.0.0" } }, - "@webassemblyjs/wast-printer": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz", - "integrity": "sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ==", - "requires": { - "@webassemblyjs/ast": "1.11.0", - "@xtuc/long": "4.2.2" - } + "@ungap/global-this": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@ungap/global-this/-/global-this-0.4.4.tgz", + "integrity": "sha512-mHkm6FvepJECMNthFuIgpAEFmPOk71UyXuIxYfjytvFTnSDBIz7jmViO+LfHI/AjrazWije0PnSP3+/NlwzqtA==" }, "@wry/context": { "version": "0.5.4", @@ -2203,16 +2325,6 @@ } } }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" - }, - "@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==" - }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2227,10 +2339,11 @@ "negotiator": "0.6.2" } }, - "acorn": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1.1.tgz", - "integrity": "sha512-xYiIVjNuqtKXMxlRMDc6mZUhXehod4a3gbZ1qRlM7icK4EbxUFNLhWoPblCvFtB2Y9CIqHP3CF/rdxLItaQv8g==" + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true }, "agent-base": { "version": "6.0.2", @@ -2262,6 +2375,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2269,11 +2383,6 @@ "uri-js": "^4.2.2" } }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" - }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -2423,6 +2532,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -2468,6 +2578,19 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-includes": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz", + "integrity": "sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.5" + } + }, "array-initial": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", @@ -2531,6 +2654,17 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, + "array.prototype.flat": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz", + "integrity": "sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, "array.prototype.flatmap": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", @@ -2553,6 +2687,12 @@ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -2873,25 +3013,6 @@ } } }, - "browserslist": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.4.tgz", - "integrity": "sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==", - "requires": { - "caniuse-lite": "^1.0.30001208", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.712", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" - }, - "dependencies": { - "colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" - } - } - }, "bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -2930,11 +3051,6 @@ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" - }, "busboy": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", @@ -3041,11 +3157,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" }, - "caniuse-lite": { - "version": "1.0.30001208", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz", - "integrity": "sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==" - }, "capital-case": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", @@ -3148,11 +3259,6 @@ } } }, - "chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" - }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -3331,11 +3437,6 @@ "delayed-stream": "~1.0.0" } }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, "common-tags": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", @@ -3422,6 +3523,12 @@ "xdg-basedir": "^4.0.0" } }, + "confusing-browser-globals": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz", + "integrity": "sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA==", + "dev": true + }, "connect-flash": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", @@ -3463,6 +3570,12 @@ "upper-case": "^2.0.2" } }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -3584,6 +3697,28 @@ "node-fetch": "2.6.1" } }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -3646,6 +3781,12 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, "default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", @@ -3776,6 +3917,16 @@ } } }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, "dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -3863,11 +4014,6 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, - "electron-to-chromium": { - "version": "1.3.712", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.712.tgz", - "integrity": "sha512-3kRVibBeCM4vsgoHHGKHmPocLqtFAGTrebXxxtgKs87hNUzXrX2NuS3jnBys7IozCnw7viQlozxKkmty2KNfrw==" - }, "elegant-spinner": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", @@ -3892,19 +4038,20 @@ "once": "^1.4.0" } }, - "enhanced-resolve": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz", - "integrity": "sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==", + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "ansi-colors": "^4.1.1" }, "dependencies": { - "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true } } }, @@ -3960,11 +4107,6 @@ } } }, - "es-module-lexer": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.4.1.tgz", - "integrity": "sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA==" - }, "es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -4036,29 +4178,402 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "eslint": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.24.0.tgz", + "integrity": "sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.21", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.4", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.8.0.tgz", + "integrity": "sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "eslint-config-airbnb-base": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", + "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.2" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } + } + }, + "eslint-config-prettier": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz", + "integrity": "sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + } + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + } + }, + "eslint-plugin-import": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", + "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-module-utils": "^2.6.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.1", + "read-pkg-up": "^2.0.0", + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, "esm": { "version": "3.2.25", "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } }, "esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "requires": { "estraverse": "^5.2.0" }, @@ -4066,14 +4581,22 @@ "estraverse": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true } } }, "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true }, "etag": { "version": "1.8.1", @@ -4085,11 +4608,6 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, "eventsource": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", @@ -4407,7 +4925,8 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "fast-glob": { "version": "3.2.5", @@ -4475,6 +4994,12 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, "fastq": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.1.tgz", @@ -4519,6 +5044,15 @@ "escape-string-regexp": "^1.0.5" } }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -4591,6 +5125,22 @@ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==" }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", + "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", + "dev": true + }, "flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", @@ -5175,6 +5725,12 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5306,11 +5862,6 @@ } } }, - "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==" - }, "glob-watcher": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", @@ -5755,32 +6306,6 @@ "through2": "^2.0.3" } }, - "gulp-tslint": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/gulp-tslint/-/gulp-tslint-8.1.4.tgz", - "integrity": "sha512-wBoZIEMJRz9urHwolsvQpngA9l931p6g/Liwz1b/KrsVP6jEBFZv/o0NS1TFCQZi/l8mXxz8+v3twhf4HOXxPQ==", - "requires": { - "@types/fancy-log": "1.3.0", - "ansi-colors": "^1.0.1", - "fancy-log": "1.3.3", - "map-stream": "~0.0.7", - "plugin-error": "1.0.1", - "through": "~2.3.8" - }, - "dependencies": { - "plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", - "requires": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" - } - } - } - }, "gulp-typescript": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.1.tgz", @@ -6247,6 +6772,15 @@ "ci-info": "^2.0.0" } }, + "is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -6541,16 +7075,6 @@ "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" }, - "jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - } - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6560,6 +7084,7 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -6575,11 +7100,6 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6589,7 +7109,8 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "json-stable-stringify": { "version": "1.0.1", @@ -6615,6 +7136,15 @@ "remove-trailing-spaces": "^1.0.6" } }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, "jsonify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", @@ -6815,6 +7345,16 @@ "flush-write-stream": "^1.0.2" } }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, "liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -7054,16 +7594,41 @@ "strip-bom": "^2.0.0" } }, - "loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==" + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "dev": true + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -7112,6 +7677,12 @@ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", "dev": true }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -7247,6 +7818,15 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -7280,11 +7860,6 @@ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" }, - "map-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", - "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=" - }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -7335,11 +7910,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7407,6 +7977,12 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, "mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -7426,21 +8002,6 @@ } } }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - } - } - }, "moment": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", @@ -7505,16 +8066,17 @@ "to-regex": "^3.0.1" } }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -7540,11 +8102,6 @@ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" }, - "node-releases": { - "version": "1.1.71", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", - "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==" - }, "nodemon": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", @@ -7822,6 +8379,18 @@ "isobject": "^3.0.0" } }, + "object.entries": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.3.tgz", + "integrity": "sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + } + }, "object.map": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", @@ -7848,6 +8417,18 @@ "make-iterator": "^1.0.0" } }, + "object.values": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.3.tgz", + "integrity": "sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2", + "has": "^1.0.3" + } + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -7887,6 +8468,20 @@ "@wry/trie": "^0.2.1" } }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, "ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -7954,6 +8549,32 @@ "yocto-queue": "^0.1.0" } }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + }, + "dependencies": { + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } + } + }, "p-map": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", @@ -8118,6 +8739,12 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", @@ -8230,7 +8857,27 @@ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "requires": { - "pinkie": "^2.0.0" + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + } } }, "plugin-error": { @@ -8307,11 +8954,23 @@ "xtend": "^4.0.0" } }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" }, + "prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true + }, "pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", @@ -8327,6 +8986,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -8381,7 +9046,8 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true }, "pupa": { "version": "2.1.1", @@ -8406,14 +9072,6 @@ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -8545,6 +9203,12 @@ "safe-regex": "^1.1.0" } }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, "registry-auth-token": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", @@ -8848,6 +9512,12 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -8921,6 +9591,15 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -8970,16 +9649,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", - "requires": { - "@types/json-schema": "^7.0.6", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, "scuid": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz", @@ -9052,14 +9721,6 @@ "upper-case-first": "^2.0.2" } }, - "serialize-javascript": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", - "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", - "requires": { - "randombytes": "^2.1.0" - } - }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", @@ -9107,6 +9768,21 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -9235,11 +9911,6 @@ } } }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -9323,7 +9994,8 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true }, "sse-z": { "version": "0.3.0", @@ -9501,10 +10173,85 @@ "node-fetch": "^2.6.1" } }, - "tapable": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", - "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==" + "table": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.9.tgz", + "integrity": "sha512-F3cLs9a3hL1Z7N4+EkSscsel3z55XT950AvB05bwayrNg5T1/gykXtigioTAjbltvbMSJvvhFCbnf6mX+ntnJQ==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "lodash.clonedeep": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" + }, + "dependencies": { + "ajv": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.1.0.tgz", + "integrity": "sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } }, "tarn": { "version": "3.0.1", @@ -9516,63 +10263,17 @@ "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==" }, - "terser": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.6.1.tgz", - "integrity": "sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw==", - "requires": { - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.19" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - } - } - }, - "terser-webpack-plugin": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.1.tgz", - "integrity": "sha512-5XNNXZiR8YO6X6KhSGXfY0QrGrCRlSwAEjIIrlRQR4W8nP69TaJUlh3bkuac6zzgspiGPfKEHcY295MMVExl5Q==", - "requires": { - "jest-worker": "^26.6.2", - "p-limit": "^3.1.0", - "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1", - "source-map": "^0.6.1", - "terser": "^5.5.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true }, "through2": { "version": "2.0.5", @@ -9774,107 +10475,45 @@ } } }, - "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" - }, - "tslint": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", - "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.8.0", - "tsutils": "^2.29.0" + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" }, "dependencies": { - "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==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "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==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "has-flag": { + "strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true } } }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, "type": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==" }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, "type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", @@ -10115,6 +10754,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "requires": { "punycode": "^2.1.0" } @@ -10164,6 +10804,12 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, "v8flags": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", @@ -10271,81 +10917,6 @@ "vinyl": "^2.0.0" } }, - "watchpack": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.1.tgz", - "integrity": "sha512-Oo7LXCmc1eE1AjyuSBmtC3+Wy4HcV8PxWh2kP6fOl8yTlNS7r0K9l1ao2lrrUza7V39Y3D/BbJgY8VeSlc5JKw==", - "requires": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - } - }, - "webpack": { - "version": "5.32.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.32.0.tgz", - "integrity": "sha512-jB9PrNMFnPRiZGnm/j3qfNqJmP3ViRzkuQMIf8za0dgOYvSLi/cgA+UEEGvik9EQHX1KYyGng5PgBTTzGrH9xg==", - "requires": { - "@types/eslint-scope": "^3.7.0", - "@types/estree": "^0.0.46", - "@webassemblyjs/ast": "1.11.0", - "@webassemblyjs/wasm-edit": "1.11.0", - "@webassemblyjs/wasm-parser": "1.11.0", - "acorn": "^8.0.4", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.7.0", - "es-module-lexer": "^0.4.0", - "eslint-scope": "^5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.4", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.0.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.1", - "watchpack": "^2.0.0", - "webpack-sources": "^2.1.1" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" - }, - "mime-db": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", - "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" - }, - "mime-types": { - "version": "2.1.30", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", - "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", - "requires": { - "mime-db": "1.47.0" - } - } - } - }, - "webpack-sources": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.2.0.tgz", - "integrity": "sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w==", - "requires": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, "whatwg-fetch": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", @@ -10415,6 +10986,12 @@ } } }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", @@ -10460,6 +11037,12 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==" }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/server/package.json b/server/package.json index 4051f08..9e47d24 100644 --- a/server/package.json +++ b/server/package.json @@ -18,12 +18,12 @@ "generate-gql-types": "node ./node_modules/graphql-schema-typescript/lib/cli.js generate-ts src/api --output src/api/graphql.types.ts --typePrefix \"\" --asyncResult always --requireResolverTypes true --noStringEnum true", "knex:migrate:make": "knex --knexfile src/knexfile.ts -x ts migrate:make", "knex:migrate:latest": "knex --knexfile src/knexfile.ts migrate:latest", - "knex:migrate:rollback": "knex --knexfile src/knexfile.ts migrate:rollback" + "knex:migrate:rollback": "knex --knexfile src/knexfile.ts migrate:rollback", + "lint": "eslint src/ --fix; prettier src/ --write" }, "author": "Ryan Petschek, Evan Strat", "license": "MIT", "dependencies": { - "body-parser": "^1.19.0", "chalk": "^4.1.0", "compression": "^1.7.4", "connect-flash": "^0.1.1", @@ -39,7 +39,6 @@ "graphql-subscriptions": "^1.2.1", "graphql-tools": "^7.0.4", "gulp-copy": "^4.0.1", - "gulp-tslint": "^8.1.4", "gulp-typescript": "^5.0.1", "isomorphic-fetch": "^3.0.0", "knex": "^0.95.4", @@ -50,16 +49,18 @@ "passport-oauth2": "^1.5.0", "path": "^0.12.7", "pg": "^8.5.1", + "serve-static": "^1.14.1", "subscriptions-transport-ws": "^0.9.18", "ts-node": "^9.1.1", - "tslint": "^5.20.1", - "typescript": "^4.2.4", - "webpack": "^5.32.0" + "typescript": "^4.2.4" }, "devDependencies": { "@graphql-codegen/cli": "^1.21.3", "@graphql-codegen/typescript": "1.21.1", "@graphql-codegen/typescript-resolvers": "1.19.0", + "@hex-labs/eslint-config": "^1.1.1", + "@hex-labs/prettier-config": "^1.1.1", + "@hex-labs/tsconfig": "^1.1.1", "@types/compression": "1.7.0", "@types/connect-flash": "0.0.36", "@types/connect-pg-simple": "^4.2.2", @@ -68,11 +69,18 @@ "@types/express": "^4.17.11", "@types/express-graphql": "^0.8.2", "@types/express-session": "^1.17.3", + "@types/isomorphic-fetch": "0.0.35", "@types/morgan": "^1.9.2", "@types/node": "^14.14.37", "@types/passport": "^1.0.6", "@types/passport-oauth2": "^1.4.10", "@types/pg": "^7.14.11", - "@types/ws": "^7.4.1" - } + "@types/ws": "^7.4.1", + "eslint": "^7.24.0", + "prettier": "^2.2.1" + }, + "eslintConfig": { + "extends": "@hex-labs/eslint-config" + }, + "prettier": "@hex-labs/prettier-config" } diff --git a/server/src/api/api.graphql b/server/src/api/api.graphql index 5e605d8..39bd1b5 100644 --- a/server/src/api/api.graphql +++ b/server/src/api/api.graphql @@ -1,228 +1,226 @@ type Query { - # Get information about the currently logged in user - user: User - # For admins, get information about all users - users(search: UserSearch!): [User!] - # Get information about a specific item, given its ID - item(id: Int!): Item - # Get an array of every item in the database - allItems: [ItemsByLocation!] - # Get a list containing each category that is on at least one item - categories: [Category!] - # Get detailed statistics about all items - itemStatistics: [ItemWithStatistics!] - # Return request(s) matching the search query provided and that the user has permission to see - requests(search: RequestSearch!): [Request!] - # Return setting(s) with the name provided - setting(name: String!): Setting - locations: [Location!], + # Get information about the currently logged in user + user: User + # For admins, get information about all users + users(search: UserSearch!): [User!] + # Get information about a specific item, given its ID + item(id: Int!): Item + # Get an array of every item in the database + allItems: [ItemsByLocation!] + # Get a list containing each category that is on at least one item + categories: [Category!] + # Get detailed statistics about all items + itemStatistics: [ItemWithStatistics!] + # Return request(s) matching the search query provided and that the user has permission to see + requests(search: RequestSearch!): [Request!] + # Return setting(s) with the name provided + setting(name: String!): Setting + locations: [Location!] } type Mutation { - # Create a new item - createItem(newItem: ItemInput!): Item! - # Update an existing setting - updateSetting(name: String!, updatedSetting: SettingInput!): Setting! - # Create a new setting - createSetting(newSetting: SettingInput!): Setting! - # Update the item with the specified ID to have the properties in updatedItem. At this time - # all fields must be included - updateItem(id: Int!, updatedItem: ItemInput!): Item - createRequest(newRequest: RequestInput!): Request! - updateRequest(updatedRequest: RequestUpdateInput!): Request - # Delete the request with the specified ID. Returns true if successful; otherwise, false - deleteRequest(id: Int!): Boolean! - updateUser(uuid: String!, updatedUser: UserUpdateInput!): User + # Create a new item + createItem(newItem: ItemInput!): Item! + # Update an existing setting + updateSetting(name: String!, updatedSetting: SettingInput!): Setting! + # Create a new setting + createSetting(newSetting: SettingInput!): Setting! + # Update the item with the specified ID to have the properties in updatedItem. At this time + # all fields must be included + updateItem(id: Int!, updatedItem: ItemInput!): Item + createRequest(newRequest: RequestInput!): Request! + updateRequest(updatedRequest: RequestUpdateInput!): Request + # Delete the request with the specified ID. Returns true if successful; otherwise, false + deleteRequest(id: Int!): Boolean! + updateUser(uuid: String!, updatedUser: UserUpdateInput!): User } type Subscription { - request_change: Request! + request_change: Request! } type User { - uuid: ID! - name: String! - email: String! - phone: String! - slackUsername: String! - # Whether this user's ID has been collected or not - haveID: Boolean! - # Whether this user has admin permissions - admin: Boolean! + uuid: ID! + name: String! + email: String! + phone: String! + slackUsername: String! + # Whether this user's ID has been collected or not + haveID: Boolean! + # Whether this user has admin permissions + admin: Boolean! } type Item { - id: Int! - item_name: String! - description: String! - imageUrl: String! - category: String! - location: Location! - # Total number of this item in the hardware inventory - totalAvailable: Int! - # The number of an item that is not reserved - qtyInStock: Int! - # The number of an item that should be physically at the hardware desk - qtyUnreserved: Int! - # The number of an item that is available to be allocated to requests waiting to be approved - qtyAvailableForApproval: Int - # The largest quantity of this item that can be requested in one request - maxRequestQty: Int! - # The value of this item to replace it if lost or damaged (only visible to admins) - price: Float - # Whether to show this item to non-admins on the hardware list - hidden: Boolean! - # Whether people who check out this item are expected to return it - returnRequired: Boolean! - # Whether admin approval is required before requests for this item will be processed - approvalRequired: Boolean! - # The person or organization that owns this item (only visible to admins) - owner: String - # Detailed breakdown by status for all requests for this item (only visible to admins) + id: Int! + item_name: String! + description: String! + imageUrl: String! + category: String! + location: Location! + # Total number of this item in the hardware inventory + totalAvailable: Int! + # The number of an item that is not reserved + qtyInStock: Int! + # The number of an item that should be physically at the hardware desk + qtyUnreserved: Int! + # The number of an item that is available to be allocated to requests waiting to be approved + qtyAvailableForApproval: Int + # The largest quantity of this item that can be requested in one request + maxRequestQty: Int! + # The value of this item to replace it if lost or damaged (only visible to admins) + price: Float + # Whether to show this item to non-admins on the hardware list + hidden: Boolean! + # Whether people who check out this item are expected to return it + returnRequired: Boolean! + # Whether admin approval is required before requests for this item will be processed + approvalRequired: Boolean! + # The person or organization that owns this item (only visible to admins) + owner: String + # Detailed breakdown by status for all requests for this item (only visible to admins) } type ItemWithStatistics { - item: Item! - detailedQuantities: DetailedItemQuantities! + item: Item! + detailedQuantities: DetailedItemQuantities! } type DetailedItemQuantities { - SUBMITTED: Int!, - APPROVED: Int!, - DENIED: Int!, - ABANDONED: Int!, - CANCELLED: Int!, - READY_FOR_PICKUP: Int!, - FULFILLED: Int!, - RETURNED: Int!, - LOST: Int!, - DAMAGED: Int! - total: Int! + SUBMITTED: Int! + APPROVED: Int! + DENIED: Int! + ABANDONED: Int! + CANCELLED: Int! + READY_FOR_PICKUP: Int! + FULFILLED: Int! + RETURNED: Int! + LOST: Int! + DAMAGED: Int! + total: Int! } type ItemsByLocation { - location: Location!, - categories: [ItemsByCategory!] + location: Location! + categories: [ItemsByCategory!] } type ItemsByCategory { - category: Category! - items: [Item!] + category: Category! + items: [Item!] } type Setting { - name: String! - value: String! + name: String! + value: String! } input ItemInput { - item_name: String! - description: String! - imageUrl: String! - category: String! - location: String! - # Total number of this item in the hardware inventory - totalAvailable: Int! - # The largest quantity of this item that can be requested in one request - maxRequestQty: Int! - # The value of this item to replace it if lost or damaged - price: Float! - # Whether to show this item to non-admins on the hardware list - hidden: Boolean! - # Whether people who check out this item are expected to return it - returnRequired: Boolean! - # Whether admin approval is required before requests for this item will be processed - approvalRequired: Boolean! - # The person or organization that owns this item - owner: String! + item_name: String! + description: String! + imageUrl: String! + category: String! + location: String! + # Total number of this item in the hardware inventory + totalAvailable: Int! + # The largest quantity of this item that can be requested in one request + maxRequestQty: Int! + # The value of this item to replace it if lost or damaged + price: Float! + # Whether to show this item to non-admins on the hardware list + hidden: Boolean! + # Whether people who check out this item are expected to return it + returnRequired: Boolean! + # Whether admin approval is required before requests for this item will be processed + approvalRequired: Boolean! + # The person or organization that owns this item + owner: String! } input SettingInput { - name: String! - value: String! + name: String! + value: String! } type Category { - # The unique ID for this category - category_id: Int! - # The human-readable, user-facing name for this category - category_name: String! + # The unique ID for this category + category_id: Int! + # The human-readable, user-facing name for this category + category_name: String! } type Location { - location_id: Int! - location_name: String! - location_hidden: Boolean! + location_id: Int! + location_name: String! + location_hidden: Boolean! } enum RequestStatus { - SUBMITTED - APPROVED - DENIED - ABANDONED - CANCELLED - READY_FOR_PICKUP - FULFILLED - RETURNED - LOST - DAMAGED + SUBMITTED + APPROVED + DENIED + ABANDONED + CANCELLED + READY_FOR_PICKUP + FULFILLED + RETURNED + LOST + DAMAGED } # Simple representation of a request without the linked item or user type SimpleRequest { - request_id: Int!, - status: RequestStatus!, - quantity: Int!, - createdAt: String!, - updatedAt: String! + request_id: Int! + status: RequestStatus! + quantity: Int! + createdAt: String! + updatedAt: String! } type Request { - request_id: Int!, - user: User!, - item: Item!, - location: Location!, - status: RequestStatus!, - quantity: Int!, - createdAt: String!, - updatedAt: String! + request_id: Int! + user: User! + item: Item! + location: Location! + status: RequestStatus! + quantity: Int! + createdAt: String! + updatedAt: String! } - - input RequestSearch { - item_id: Int, - request_id: Int, - user_id: String, - statuses: [RequestStatus!] + item_id: Int + request_id: Int + user_id: String + statuses: [RequestStatus!] } input RequestInput { - user_id: String!, - request_item_id: Int!, - quantity: Int! + user_id: String! + request_item_id: Int! + quantity: Int! } input RequestUpdateInput { - request_id: Int!, - new_quantity: Int, - new_status: RequestStatus, - user_haveID: Boolean + request_id: Int! + new_quantity: Int + new_status: RequestStatus + user_haveID: Boolean } input UserSearch { - uuid: String, - name: String, - haveID: Boolean, - phone: String, - email: String, - slackUsername: String, - admin: Boolean + uuid: String + name: String + haveID: Boolean + phone: String + email: String + slackUsername: String + admin: Boolean } input UserUpdateInput { - phone: String, - slackUsername: String, - haveID: Boolean, - admin: Boolean + phone: String + slackUsername: String + haveID: Boolean + admin: Boolean } diff --git a/server/src/api/api.ts b/server/src/api/api.ts index 8460137..f4a6862 100644 --- a/server/src/api/api.ts +++ b/server/src/api/api.ts @@ -1,19 +1,28 @@ /* tslint:disable:variable-name */ import * as fs from "fs"; import * as path from "path"; - import express from "express"; import { graphqlHTTP } from "express-graphql"; -import {GraphQLError} from "graphql"; -import {DB, IItem} from "../database"; -import {isAdminNoAuthCheck} from "../auth/auth"; -import {localTimestamp, nestedRequest, onlyIfAdmin, toSimpleRequest} from "./requests"; -import {makeExecutableSchema} from "graphql-tools"; -import {Category, Item, Location, Request, RequestStatus, Setting, User, UserUpdateInput} from "./graphql.types"; -import {config} from "../common"; -import {PubSub} from "graphql-subscriptions"; -import {Quantity} from "./requests/quantity"; -import {getItemLocation, ItemController} from "./items/ItemController"; +import { GraphQLError } from "graphql"; +import { makeExecutableSchema } from "graphql-tools"; +import { PubSub } from "graphql-subscriptions"; + +import { DB, IItem } from "../database"; +import { isAdminNoAuthCheck } from "../auth/auth"; +import { localTimestamp, nestedRequest, onlyIfAdmin, toSimpleRequest } from "./requests"; +import { + Category, + Item, + Location, + Request, + RequestStatus, + Setting, + User, + UserUpdateInput, +} from "./graphql.types"; +import { config } from "../common"; +import { Quantity } from "./requests/quantity"; +import { getItemLocation, ItemController } from "./items/ItemController"; const fetch = require("isomorphic-fetch"); const bodyParser = require("body-parser"); @@ -22,778 +31,844 @@ export const apiRoutes = express.Router(); export const pubsub = new PubSub(); async function getItem(itemId: number, isAdmin: boolean): Promise { - if (itemId <= 0) { - throw new GraphQLError("Invalid item ID. The item ID you provided was <= 0, but item IDs must be >= 1."); - } - - const item: IItem[] = await DB.from("items") - .join("categories", "items.category_id", "=", "categories.category_id") - .join("locations", "locations.location_id", "=", "items.location_id") - .where({item_id: itemId}); - - if (item.length === 0) { - return null; - } - const actualItem: any = item[0]; - const {item_id} = actualItem; - const {qtyInStock, qtyUnreserved, qtyAvailableForApproval} = await Quantity.all([item_id]); - return { - ...actualItem, - id: item_id, - category: actualItem.category_name, - location: getItemLocation(actualItem), - price: onlyIfAdmin(actualItem.price, isAdmin), - owner: onlyIfAdmin(actualItem.owner, isAdmin), - qtyInStock: qtyInStock[item_id], - qtyUnreserved: qtyUnreserved[item_id], - qtyAvailableForApproval: qtyAvailableForApproval[item_id] - }; + if (itemId <= 0) { + throw new GraphQLError( + "Invalid item ID. The item ID you provided was <= 0, but item IDs must be >= 1." + ); + } + + const item: IItem[] = await DB.from("items") + .join("categories", "items.category_id", "=", "categories.category_id") + .join("locations", "locations.location_id", "=", "items.location_id") + .where({ item_id: itemId }); + + if (item.length === 0) { + return null; + } + const actualItem: any = item[0]; + const { item_id } = actualItem; + const { qtyInStock, qtyUnreserved, qtyAvailableForApproval } = await Quantity.all([item_id]); + return { + ...actualItem, + id: item_id, + category: actualItem.category_name, + location: getItemLocation(actualItem), + price: onlyIfAdmin(actualItem.price, isAdmin), + owner: onlyIfAdmin(actualItem.owner, isAdmin), + qtyInStock: qtyInStock[item_id], + qtyUnreserved: qtyUnreserved[item_id], + qtyAvailableForApproval: qtyAvailableForApproval[item_id], + }; } const REQUEST_CHANGE = "request_change"; async function getUser(userId: string) { - const users: User[] = await DB.from("users").where({ - uuid: userId - }).select("name", "email", "uuid", "phone", "slackUsername", "haveID", "admin"); - - if (users.length === 0) { - throw new GraphQLError("Unable to create this request because no user with the UUID provided was found"); - } - - return users[0]; + const users: User[] = await DB.from("users") + .where({ + uuid: userId, + }) + .select("name", "email", "uuid", "phone", "slackUsername", "haveID", "admin"); + + if (users.length === 0) { + throw new GraphQLError( + "Unable to create this request because no user with the UUID provided was found" + ); + } + + return users[0]; } // tslint:disable-next-line:typedef async function updateUser(args, context) { - const searchObj: UserUpdateInput = args.updatedUser; - - if (!context.user.admin && args.uuid !== context.user.uuid) { - throw new GraphQLError("You do not have permission to update users other than yourself."); - } - - // non-admins can't change these properties - if (!context.user.admin) { - console.log("updateUser: user is not admin"); - delete searchObj.admin; - delete searchObj.haveID; - } - - // don't let an admin remove their own admin permissions - if (context.user.admin && args.uuid === context.user.uuid) { - delete searchObj.admin; - } - - // stop if no properties are going to be updated - if (!Object.keys(searchObj).length) { - console.log("updateUser: stopping as no properties will be updated"); - return null; - } - - if (searchObj.phone && !(/^\(?(\d){3}\)? ?(\d){3}-?(\d){4}$/).test(searchObj.phone)) { - throw new GraphQLError("User not updated because phone number format is invalid"); - } - - const updatedUser: User[] = await DB.from("users") - .where({ - uuid: args.uuid, - }) - .update(searchObj) - .returning(["uuid", "name", "email", "phone", "slackUsername", "haveID", "admin"]); - - if (!updatedUser.length) { - return null; - } - - return updatedUser[0]; + const searchObj: UserUpdateInput = args.updatedUser; + + if (!context.user.admin && args.uuid !== context.user.uuid) { + throw new GraphQLError("You do not have permission to update users other than yourself."); + } + + // non-admins can't change these properties + if (!context.user.admin) { + console.log("updateUser: user is not admin"); + delete searchObj.admin; + delete searchObj.haveID; + } + + // don't let an admin remove their own admin permissions + if (context.user.admin && args.uuid === context.user.uuid) { + delete searchObj.admin; + } + + // stop if no properties are going to be updated + if (!Object.keys(searchObj).length) { + console.log("updateUser: stopping as no properties will be updated"); + return null; + } + + if (searchObj.phone && !/^\(?(\d){3}\)? ?(\d){3}-?(\d){4}$/.test(searchObj.phone)) { + throw new GraphQLError("User not updated because phone number format is invalid"); + } + + const updatedUser: User[] = await DB.from("users") + .where({ + uuid: args.uuid, + }) + .update(searchObj) + .returning(["uuid", "name", "email", "phone", "slackUsername", "haveID", "admin"]); + + if (!updatedUser.length) { + return null; + } + + return updatedUser[0]; } async function getSetting(settingName: string) { - const settings: Setting[] = await DB.from("settings").where({ - name: settingName - }).select("name", "value"); - - if (settings.length === 0) { - throw new GraphQLError("No setting found"); - } - return settings[0]; + const settings: Setting[] = await DB.from("settings") + .where({ + name: settingName, + }) + .select("name", "value"); + + if (settings.length === 0) { + throw new GraphQLError("No setting found"); + } + return settings[0]; } -async function findOrCreate(tableName: string, searchObj: object, data: object, idFieldName: string): Promise { - const matchingRows = await DB.from(tableName).where(searchObj); - - if (!matchingRows.length) { - // The result must be an exact match - const id = await DB.from(tableName).returning(idFieldName).insert(data); - return id[0]; - } - return matchingRows[0][idFieldName]; +async function findOrCreate( + tableName: string, + searchObj: object, + data: object, + idFieldName: string +): Promise { + const matchingRows = await DB.from(tableName).where(searchObj); + + if (!matchingRows.length) { + // The result must be an exact match + const id = await DB.from(tableName).returning(idFieldName).insert(data); + return id[0]; + } + return matchingRows[0][idFieldName]; } const resolvers: any = { - Query: { - /* Queries */ - /** - * Returns information about the current signed in user - * Access level: current signed in user - * @param root - * @param args - * @param context - */ - user: async (root, args, context): Promise => { - return { - uuid: context.user.uuid, - name: context.user.name, - email: context.user.email, - phone: context.user.phone, - slackUsername: context.user.slackUsername, - haveID: context.user.haveID, - admin: context.user.admin - }; - }, - users: async (root, args, context): Promise => { - let searchObj = args.search; - - // Restrict results to current user for non-admins - if (!context.user.admin) { - searchObj = { - uuid: context.user.uuid - }; - } - - const colNames: string[] = ["uuid", "name", "haveID", "phone", - "email", "slackUsername", "admin"]; - - return await DB.from("users") - .where(searchObj) - .select(colNames) - .orderBy("name"); - }, - /** - * Returns information about a single item - * TODO: better filtering/search - * Access level: any signed in user - * @param root - * @param args - * @param context - */ - item: async (root, args, context): Promise => { - return await getItem(args.id, context.user.admin); - }, - /** - * Bulk items API - * TODO: pagination/returned quantity limit - * Access level: any signed in user - * @param root - * @param args - * @param context - */ - allItems: async (root, args, context): Promise => { - const itemsSearchObj: any = {}; - const locationsSearchObj: any = {}; - if (!context.user.admin) { - itemsSearchObj.hidden = false; - locationsSearchObj.location_hidden = false; - } - const items = await DB.from("items") - .where(itemsSearchObj) - .join("categories", "items.category_id", "=", "categories.category_id"); - - const locations: Location[] = await DB.from("locations") - .where(locationsSearchObj); - - const {qtyInStock, qtyUnreserved, qtyAvailableForApproval} = await Quantity.all(); - const itemsByLocation = {}; - for (let i = 0; i < locations.length; i++) { - const loc = locations[i]; - - - const itemsAtLocation = items.filter(predItem => predItem.location_id === loc.location_id); - const itemsByCategory = {}; - - for (let j = 0; j < itemsAtLocation.length; j++) { - const item = itemsAtLocation[j]; - - if (!itemsByCategory.hasOwnProperty(item.category_id)) { - itemsByCategory[item.category_id] = { - category: { - category_id: item.category_id, - category_name: item.category_name, - }, - items: [] - }; - } - itemsByCategory[item.category_id].items.push( - { - ...item, - id: item.item_id, - category: item.category_name, - location: loc, - price: onlyIfAdmin(item.price, context.user.admin), - owner: onlyIfAdmin(item.owner, context.user.admin), - qtyInStock: qtyInStock[item.item_id], - qtyUnreserved: qtyUnreserved[item.item_id], - qtyAvailableForApproval: qtyAvailableForApproval[item.item_id] - } - ); - } - - itemsByLocation[loc.location_id] = { - location: loc, - categories: Object.values(itemsByCategory).sort((a: any, b: any) => { - return a.category.category_name.localeCompare(b.category.category_name); - }) - }; - - } - - return Object.values(itemsByLocation); - }, - categories: (root, args, context): Promise => { - return DB.from("categories"); - }, - locations: (root, args, context): Promise => { - return DB.from("locations"); - }, - itemStatistics: async (root, args, context): Promise => { - if (!context.user.admin) { // TODO: validate this - throw new GraphQLError("You do not have permission to access this endpoint"); - } - - const items: any = await ItemController.get({}, context.user.admin); - const detailedQuantities = await Quantity.quantityStatistics(); - - return items.map((item) => { - const qtyInfo = detailedQuantities[item.item_id] || { - SUBMITTED: 0, - APPROVED: 0, - DENIED: 0, - ABANDONED: 0, - CANCELLED: 0, - READY_FOR_PICKUP: 0, - FULFILLED: 0, - RETURNED: 0, - LOST: 0, - DAMAGED: 0, - total: 0 - - }; - return { - item, - detailedQuantities: qtyInfo - }; - }); - }, - requests: async (root, args, context): Promise => { - const searchObj: any = {}; - - - if (args.search.item_id) { - searchObj.item_id = args.search.item_id; - } - - if (args.search.request_id) { - searchObj.request_id = args.search.request_id; - } - - if (args.search.user_id) { - searchObj.user_id = args.search.user_id; - } - - let statuses: RequestStatus[] = ["SUBMITTED", - "APPROVED", - "DENIED", - "ABANDONED", - "CANCELLED", - "READY_FOR_PICKUP", - "FULFILLED", - "RETURNED", - "LOST", - "DAMAGED"]; - - if (args.search.statuses && args.search.statuses.length) { - statuses = args.search.statuses; - } - - // If user is not an admin - if (!context.user.admin) { - // then if they are requesting requests for a user that is not themselves - if (args.search.user_id && args.search.user_id !== context.user.uuid) { - // return an empty array and avoid making a DB query - return []; - } - - // otherwise, restrict their results to just their user ID - searchObj.user_id = context.user.uuid; - searchObj.location_hidden = false; // don't show hidden locations - searchObj.hidden = false; // don't show hidden items - } - - const requests = await DB.from("requests") - .whereIn("status", statuses) - .andWhere(searchObj) - .join("users", "requests.user_id", "=", "users.uuid") - .join("items", "requests.request_item_id", "=", "items.item_id") - .join("categories", "categories.category_id", "=", "items.category_id") - .join("locations", "locations.location_id", "=", "items.location_id") - .orderBy("requests.created_at", "asc"); - - - const items: number[] = []; - - requests.forEach((value) => { - if (items.indexOf(value.request_item_id) === -1) { - items.push(value.request_item_id); - } - }); - - const {qtyInStock, qtyUnreserved, qtyAvailableForApproval} = await Quantity.all(items); - - return requests.map(request => nestedRequest(request, context.user.admin, qtyInStock, qtyAvailableForApproval, qtyUnreserved)); - }, - setting: async (root, args, context): Promise => { - return await getSetting(args.name); - } + Query: { + /* Queries */ + /** + * Returns information about the current signed in user + * Access level: current signed in user + * @param root + * @param args + * @param context + */ + user: async (root, args, context): Promise => ({ + uuid: context.user.uuid, + name: context.user.name, + email: context.user.email, + phone: context.user.phone, + slackUsername: context.user.slackUsername, + haveID: context.user.haveID, + admin: context.user.admin, + }), + users: async (root, args, context): Promise => { + let searchObj = args.search; + + // Restrict results to current user for non-admins + if (!context.user.admin) { + searchObj = { + uuid: context.user.uuid, + }; + } + + const colNames: string[] = [ + "uuid", + "name", + "haveID", + "phone", + "email", + "slackUsername", + "admin", + ]; + + return await DB.from("users").where(searchObj).select(colNames).orderBy("name"); }, - Mutation: { - /* Mutations */ - /** - * Create a new item - * TODO: error handling? - * Access level: admins - * @param root - * @param args - * @param context - */ - createItem: async (root, args, context): Promise => { - // Restrict endpoint to admins - if (!context.user.admin) { - throw new GraphQLError("You do not have permission to access the createItem endpoint"); - } - - if (!args.newItem.item_name.trim().length) { - throw new GraphQLError("The item name (item_name) can't be empty."); - } - - if (!args.newItem.category.trim().length) { - throw new GraphQLError("The category for this item can't be blank."); - } - - if (!args.newItem.location.trim().length) { - throw new GraphQLError("The location for this item can't be blank."); - } - - if (args.newItem.totalAvailable < 0) { - throw new GraphQLError(`The total quantity available (totalQtyAvailable) for a new item can't be less than 0. Value provided: ${args.newItem.totalAvailable}`); - } - - if (args.newItem.maxRequestQty < 1) { - throw new GraphQLError(`The max request quantity (maxRequestQty) must be at least 1. Value provided: ${args.newItem.maxRequestQty}`); - } - - if (args.newItem.maxRequestQty > args.newItem.totalAvailable) { - throw new GraphQLError(`The max request quantity (maxRequestQty) can't be greater than the total quantity of this item (totalAvailable) that is available. maxRequestQty: ${args.newItem.maxRequestQty}, totalAvailable: ${args.newItem.totalAvailable}`); - } - - const categoryObj = {category_name: args.newItem.category}; - const category_id = await findOrCreate("categories", categoryObj, categoryObj, "category_id"); - - const locationObj = { - location_name: args.newItem.location + /** + * Returns information about a single item + * TODO: better filtering/search + * Access level: any signed in user + * @param root + * @param args + * @param context + */ + item: async (root, args, context): Promise => + await getItem(args.id, context.user.admin), + /** + * Bulk items API + * TODO: pagination/returned quantity limit + * Access level: any signed in user + * @param root + * @param args + * @param context + */ + allItems: async (root, args, context): Promise => { + const itemsSearchObj: any = {}; + const locationsSearchObj: any = {}; + if (!context.user.admin) { + itemsSearchObj.hidden = false; + locationsSearchObj.location_hidden = false; + } + const items = await DB.from("items") + .where(itemsSearchObj) + .join("categories", "items.category_id", "=", "categories.category_id"); + + const locations: Location[] = await DB.from("locations").where(locationsSearchObj); + + const { qtyInStock, qtyUnreserved, qtyAvailableForApproval } = await Quantity.all(); + const itemsByLocation = {}; + for (let i = 0; i < locations.length; i++) { + const loc = locations[i]; + + const itemsAtLocation = items.filter(predItem => predItem.location_id === loc.location_id); + const itemsByCategory = {}; + + for (let j = 0; j < itemsAtLocation.length; j++) { + const item = itemsAtLocation[j]; + + if (!itemsByCategory.hasOwnProperty(item.category_id)) { + itemsByCategory[item.category_id] = { + category: { + category_id: item.category_id, + category_name: item.category_name, + }, + items: [], }; - const location_id = await findOrCreate("locations", locationObj, locationObj, "location_id"); + } + itemsByCategory[item.category_id].items.push({ + ...item, + id: item.item_id, + category: item.category_name, + location: loc, + price: onlyIfAdmin(item.price, context.user.admin), + owner: onlyIfAdmin(item.owner, context.user.admin), + qtyInStock: qtyInStock[item.item_id], + qtyUnreserved: qtyUnreserved[item.item_id], + qtyAvailableForApproval: qtyAvailableForApproval[item.item_id], + }); + } - delete args.newItem.category; // Remove the category property from the input item so knex won't try to add it to the database - delete args.newItem.location; + itemsByLocation[loc.location_id] = { + location: loc, + categories: Object.values(itemsByCategory).sort((a: any, b: any) => + a.category.category_name.localeCompare(b.category.category_name) + ), + }; + } - const newObjId = await DB.from("items").returning("item_id").insert({ - category_id, - location_id, - ...args.newItem - }); + return Object.values(itemsByLocation); + }, + categories: (root, args, context): Promise => DB.from("categories"), + locations: (root, args, context): Promise => DB.from("locations"), + itemStatistics: async (root, args, context): Promise => { + if (!context.user.admin) { + // TODO: validate this + throw new GraphQLError("You do not have permission to access this endpoint"); + } + + const items: any = await ItemController.get({}, context.user.admin); + const detailedQuantities = await Quantity.quantityStatistics(); + + return items.map(item => { + const qtyInfo = detailedQuantities[item.item_id] || { + SUBMITTED: 0, + APPROVED: 0, + DENIED: 0, + ABANDONED: 0, + CANCELLED: 0, + READY_FOR_PICKUP: 0, + FULFILLED: 0, + RETURNED: 0, + LOST: 0, + DAMAGED: 0, + total: 0, + }; + return { + item, + detailedQuantities: qtyInfo, + }; + }); + }, + requests: async (root, args, context): Promise => { + const searchObj: any = {}; + + if (args.search.item_id) { + searchObj.item_id = args.search.item_id; + } + + if (args.search.request_id) { + searchObj.request_id = args.search.request_id; + } + + if (args.search.user_id) { + searchObj.user_id = args.search.user_id; + } + + let statuses: RequestStatus[] = [ + "SUBMITTED", + "APPROVED", + "DENIED", + "ABANDONED", + "CANCELLED", + "READY_FOR_PICKUP", + "FULFILLED", + "RETURNED", + "LOST", + "DAMAGED", + ]; + + if (args.search.statuses && args.search.statuses.length) { + statuses = args.search.statuses; + } + + // If user is not an admin + if (!context.user.admin) { + // then if they are requesting requests for a user that is not themselves + if (args.search.user_id && args.search.user_id !== context.user.uuid) { + // return an empty array and avoid making a DB query + return []; + } - console.log("The new item's ID is", newObjId[0]); + // otherwise, restrict their results to just their user ID + searchObj.user_id = context.user.uuid; + searchObj.location_hidden = false; // don't show hidden locations + searchObj.hidden = false; // don't show hidden items + } + + const requests = await DB.from("requests") + .whereIn("status", statuses) + .andWhere(searchObj) + .join("users", "requests.user_id", "=", "users.uuid") + .join("items", "requests.request_item_id", "=", "items.item_id") + .join("categories", "categories.category_id", "=", "items.category_id") + .join("locations", "locations.location_id", "=", "items.location_id") + .orderBy("requests.created_at", "asc"); - return await getItem(newObjId[0], context.user.admin); - }, - /** - * Update an existing item given its ID - * TODO: reduce duplicate code from createItem - * TODO: should be refactored to be like updateRequest - * @param root - * @param args - * @param context - */ - updateItem: async (root, args, context): Promise => { - // Restrict endpoint to admins - if (!context.user.admin) { - throw new GraphQLError("You do not have permission to access the updateItem endpoint"); - } - - if (!args.id || args.id <= 0) { - throw new GraphQLError("You must provide a valid item ID (greater than or equal to 0) to update an item."); - } - - if (!args.updatedItem.item_name.trim().length) { - throw new GraphQLError("The item name (item_name) can't be empty."); - } - - if (!args.updatedItem.category.trim().length) { - throw new GraphQLError("The category for this item can't be blank."); - } - - if (!args.updatedItem.location.trim().length) { - throw new GraphQLError("The location for this item can't be blank."); - } - - if (args.updatedItem.totalAvailable < 0) { - throw new GraphQLError(`The total quantity available (totalQtyAvailable) for a new item can't be less than 0. Value provided: ${args.updatedItem.totalAvailable}`); - } - - if (args.updatedItem.maxRequestQty < 1) { - throw new GraphQLError(`The max request quantity (maxRequestQty) must be at least 1. Value provided: ${args.updatedItem.maxRequestQty}`); - } - - if (args.updatedItem.maxRequestQty > args.updatedItem.totalAvailable) { - throw new GraphQLError(`The max request quantity (maxRequestQty) can't be greater than the total quantity of this item (totalAvailable) that is available. maxRequestQty: ${args.updatedItem.maxRequestQty}, totalAvailable: ${args.updatedItem.totalAvailable}`); - } - - const categoryObj = {category_name: args.updatedItem.category}; - const category_id = await findOrCreate("categories", categoryObj, categoryObj, "category_id"); - - const locationObj = { - location_name: args.updatedItem.location - }; - const location_id = await findOrCreate("locations", locationObj, locationObj, "location_id"); - - delete args.updatedItem.category; // Remove the category property from the input item so knex won't try to add it to the database - delete args.updatedItem.location; - await DB.from("items") - .where({item_id: args.id}) - .update({ - category_id, - location_id, - ...args.updatedItem - }); - - return await getItem(args.id, context.user.admin); - }, - createRequest: async (root, args, context): Promise => { - // if non-admin, user on request must be user signed in - if (!context.user.admin && context.user.uuid !== args.newRequest.user_id) { - throw new GraphQLError("Unable to create request because you are not an admin and your UUID " + - "does not match the UUID of the user this request is for"); - } - - const user = await getUser(args.newRequest.user_id); - - // check requests_allowed setting status - let requests_allowed; - try { - requests_allowed = await getSetting("requests_allowed"); - } catch (error) { - console.log("Could not find requests_allowed setting"); - } - // tslint:disable-next-line:triple-equals - if (requests_allowed !== undefined && requests_allowed.value == "false") { - console.log("Requests are disabled at this time"); - throw new GraphQLError("Requests are disabled at this time"); - } - - // fetch the item - const item: Item | null = await getItem(args.newRequest.request_item_id, context.user.admin); - - if (!item) { - throw new GraphQLError(`Can't create request for item that doesn't exist! Item ID provided: ${args.newRequest.request_item_id}`); - } - - // clip item quantity to allowed values - if (args.newRequest.quantity > item.maxRequestQty) { - console.log("clipping request quantity (too high), original:", args.newRequest.quantity, ", new:", item.maxRequestQty); - args.newRequest.quantity = item.maxRequestQty; - } else if (args.newRequest.quantity < 1) { - console.log("clipping request quantity (too low), original:", args.newRequest.quantity, ", new:", 1); - args.newRequest.quantity = 1; - } - - const initialStatus: RequestStatus = !item.approvalRequired && item.qtyUnreserved >= args.newRequest.quantity - ? "APPROVED" : "SUBMITTED"; - - let newRequest: any = await DB.from("requests").insert({ - ...args.newRequest, - status: initialStatus - }) - .returning(["request_id", "quantity", "status", "created_at", "updated_at"]); - - newRequest = newRequest[0]; - const updatedItem = await getItem(args.newRequest.request_item_id, context.user.admin); - if (!updatedItem) { - throw new GraphQLError("Unable to retrieve the new item information after creating request"); - } - - const simpleRequest = toSimpleRequest(newRequest); - - const result: Request = { - ...simpleRequest, - user, - item: updatedItem, - location: updatedItem.location - }; + const items: number[] = []; - pubsub.publish(REQUEST_CHANGE, { - [REQUEST_CHANGE]: result - }); - - return { - request_id: newRequest.request_id, - quantity: args.newRequest.quantity, - status: initialStatus, - item: updatedItem, - location: updatedItem.location, - user, - createdAt: localTimestamp(newRequest.created_at), - updatedAt: localTimestamp(newRequest.updated_at) - }; - }, - deleteRequest: async (root, args, context): Promise => { - if (!context.user.admin) { - throw new GraphQLError("You do not have permission to access the deleteRequest endpoint."); - } - - const numRowsAffected: number = await DB.from("requests") - .where({ - request_id: args.id, - }) - .del(); - - return numRowsAffected !== 0; - }, - updateRequest: async (root, args, context): Promise => { - if (!context.user.admin) { - throw new GraphQLError("You do not have permission to access the updateRequest endpoint."); - } - - const updateObj: any = {}; - - // Not going to validate against maxRequestQty since only admins can change this currently - - const newQuantity: number | undefined = args.updatedRequest.new_quantity; - if (newQuantity && newQuantity <= 0) { - throw new GraphQLError(`Invalid new requested quantity of ${newQuantity} specified. The new requested quantity must be >= 1.`); - } - - // TODO: status change validation logic - if (args.updatedRequest.new_status) { - updateObj.status = args.updatedRequest.new_status; - } - - if (args.updatedRequest.new_quantity) { - updateObj.quantity = args.updatedRequest.new_quantity; - } - - let updatedUserHaveID = null; - if (typeof args.updatedRequest.user_haveID !== "undefined") { - updatedUserHaveID = args.updatedRequest.user_haveID; - } - - if (Object.keys(updateObj).length >= 1) { - updateObj.updated_at = new Date(); - - const updatedRequest: any = await DB.from("requests") - .where({ - request_id: args.updatedRequest.request_id, - }) - .update(updateObj) - .returning(["request_id", "quantity", "status", "created_at", "updated_at", "user_id", "request_item_id"]); - - const simpleRequest = toSimpleRequest(updatedRequest[0]); - console.log(simpleRequest); - - let user; - if (updatedUserHaveID !== null) { - user = await updateUser({ - uuid: updatedRequest[0].user_id, - updatedUser: { - haveID: updatedUserHaveID - } - }, context); - } else { - user = await getUser(updatedRequest[0].user_id); - } - - // fetch the item - const item: Item | null = await getItem(updatedRequest[0].request_item_id, context.user.admin); - - const location: Location | null = getItemLocation(item); - - if (!item) { - throw new GraphQLError(`Can't create request for item that doesn't exist! Item ID provided: ${args.newRequest.request_item_id}`); - } - - if (updatedRequest.length === 0) { - return null; - } - - const result: Request = { - ...simpleRequest, - user, - item, - location - }; - - pubsub.publish(REQUEST_CHANGE, { - [REQUEST_CHANGE]: result - }); - - return result; - } - - return null; - }, - updateUser: async (root, args, context): Promise => { - return await updateUser(args, context); - }, - createSetting: async (root, args, context): Promise => { - // Restrict endpoint to admins - if (!context.user.admin) { - throw new GraphQLError("You do not have permission to access the createSetting endpoint"); - } - - if (!args.newSetting.name.trim().length) { - throw new GraphQLError("The setting name (name) can't be empty."); - } - - if (!args.newSetting.value.trim().length) { - throw new GraphQLError("The value for this setting can't be blank."); - } - - const newObjName = await DB.from("settings").insert({ - name: args.newSetting.name, - value: args.newSetting.value - }).returning("name"); - - console.log("The new setting's name is", newObjName[0]); - - return { - name: newObjName[0], - value: args.newSetting.value - }; - }, - /** - * Update an existing setting given its name - * @param root - * @param args - * @param context - */ - updateSetting: async (root, args, context): Promise => { - // Restrict endpoint to admins - if (!context.user.admin) { - throw new GraphQLError("You do not have permission to access the updateSetting endpoint"); - } - - if (!args.name.trim().length) { - throw new GraphQLError("You must provide a valid setting name to update a setting."); - } - - if (!args.updatedSetting.value.trim().length) { - throw new GraphQLError("The value can't be empty."); - } - - await DB.from("settings") - .where({name: args.name}) - .update({ - name: args.updatedSetting.name, - value: args.updatedSetting.value - }); - - return { - name: args.updatedSetting.name, - value: args.updatedSetting.value - }; + requests.forEach(value => { + if (items.indexOf(value.request_item_id) === -1) { + items.push(value.request_item_id); } + }); + + const { qtyInStock, qtyUnreserved, qtyAvailableForApproval } = await Quantity.all(items); + + return requests.map(request => + nestedRequest( + request, + context.user.admin, + qtyInStock, + qtyAvailableForApproval, + qtyUnreserved + ) + ); }, - Subscription: { - request_change: { - subscribe: () => { - return pubsub.asyncIterator(REQUEST_CHANGE); - }, - resolve: payload => { - return payload[REQUEST_CHANGE]; + setting: async (root, args, context): Promise => await getSetting(args.name), + }, + Mutation: { + /* Mutations */ + /** + * Create a new item + * TODO: error handling? + * Access level: admins + * @param root + * @param args + * @param context + */ + createItem: async (root, args, context): Promise => { + // Restrict endpoint to admins + if (!context.user.admin) { + throw new GraphQLError("You do not have permission to access the createItem endpoint"); + } + + if (!args.newItem.item_name.trim().length) { + throw new GraphQLError("The item name (item_name) can't be empty."); + } + + if (!args.newItem.category.trim().length) { + throw new GraphQLError("The category for this item can't be blank."); + } + + if (!args.newItem.location.trim().length) { + throw new GraphQLError("The location for this item can't be blank."); + } + + if (args.newItem.totalAvailable < 0) { + throw new GraphQLError( + `The total quantity available (totalQtyAvailable) for a new item can't be less than 0. Value provided: ${args.newItem.totalAvailable}` + ); + } + + if (args.newItem.maxRequestQty < 1) { + throw new GraphQLError( + `The max request quantity (maxRequestQty) must be at least 1. Value provided: ${args.newItem.maxRequestQty}` + ); + } + + if (args.newItem.maxRequestQty > args.newItem.totalAvailable) { + throw new GraphQLError( + `The max request quantity (maxRequestQty) can't be greater than the total quantity of this item (totalAvailable) that is available. maxRequestQty: ${args.newItem.maxRequestQty}, totalAvailable: ${args.newItem.totalAvailable}` + ); + } + + const categoryObj = { category_name: args.newItem.category }; + const category_id = await findOrCreate("categories", categoryObj, categoryObj, "category_id"); + + const locationObj = { + location_name: args.newItem.location, + }; + const location_id = await findOrCreate("locations", locationObj, locationObj, "location_id"); + + delete args.newItem.category; // Remove the category property from the input item so knex won't try to add it to the database + delete args.newItem.location; + + const newObjId = await DB.from("items") + .returning("item_id") + .insert({ + category_id, + location_id, + ...args.newItem, + }); + + console.log("The new item's ID is", newObjId[0]); + + return await getItem(newObjId[0], context.user.admin); + }, + /** + * Update an existing item given its ID + * TODO: reduce duplicate code from createItem + * TODO: should be refactored to be like updateRequest + * @param root + * @param args + * @param context + */ + updateItem: async (root, args, context): Promise => { + // Restrict endpoint to admins + if (!context.user.admin) { + throw new GraphQLError("You do not have permission to access the updateItem endpoint"); + } + + if (!args.id || args.id <= 0) { + throw new GraphQLError( + "You must provide a valid item ID (greater than or equal to 0) to update an item." + ); + } + + if (!args.updatedItem.item_name.trim().length) { + throw new GraphQLError("The item name (item_name) can't be empty."); + } + + if (!args.updatedItem.category.trim().length) { + throw new GraphQLError("The category for this item can't be blank."); + } + + if (!args.updatedItem.location.trim().length) { + throw new GraphQLError("The location for this item can't be blank."); + } + + if (args.updatedItem.totalAvailable < 0) { + throw new GraphQLError( + `The total quantity available (totalQtyAvailable) for a new item can't be less than 0. Value provided: ${args.updatedItem.totalAvailable}` + ); + } + + if (args.updatedItem.maxRequestQty < 1) { + throw new GraphQLError( + `The max request quantity (maxRequestQty) must be at least 1. Value provided: ${args.updatedItem.maxRequestQty}` + ); + } + + if (args.updatedItem.maxRequestQty > args.updatedItem.totalAvailable) { + throw new GraphQLError( + `The max request quantity (maxRequestQty) can't be greater than the total quantity of this item (totalAvailable) that is available. maxRequestQty: ${args.updatedItem.maxRequestQty}, totalAvailable: ${args.updatedItem.totalAvailable}` + ); + } + + const categoryObj = { category_name: args.updatedItem.category }; + const category_id = await findOrCreate("categories", categoryObj, categoryObj, "category_id"); + + const locationObj = { + location_name: args.updatedItem.location, + }; + const location_id = await findOrCreate("locations", locationObj, locationObj, "location_id"); + + delete args.updatedItem.category; // Remove the category property from the input item so knex won't try to add it to the database + delete args.updatedItem.location; + await DB.from("items") + .where({ item_id: args.id }) + .update({ + category_id, + location_id, + ...args.updatedItem, + }); + + return await getItem(args.id, context.user.admin); + }, + createRequest: async (root, args, context): Promise => { + // if non-admin, user on request must be user signed in + if (!context.user.admin && context.user.uuid !== args.newRequest.user_id) { + throw new GraphQLError( + "Unable to create request because you are not an admin and your UUID " + + "does not match the UUID of the user this request is for" + ); + } + + const user = await getUser(args.newRequest.user_id); + + // check requests_allowed setting status + let requests_allowed; + try { + requests_allowed = await getSetting("requests_allowed"); + } catch (error) { + console.log("Could not find requests_allowed setting"); + } + // tslint:disable-next-line:triple-equals + if (requests_allowed !== undefined && requests_allowed.value == "false") { + console.log("Requests are disabled at this time"); + throw new GraphQLError("Requests are disabled at this time"); + } + + // fetch the item + const item: Item | null = await getItem(args.newRequest.request_item_id, context.user.admin); + + if (!item) { + throw new GraphQLError( + `Can't create request for item that doesn't exist! Item ID provided: ${args.newRequest.request_item_id}` + ); + } + + // clip item quantity to allowed values + if (args.newRequest.quantity > item.maxRequestQty) { + console.log( + "clipping request quantity (too high), original:", + args.newRequest.quantity, + ", new:", + item.maxRequestQty + ); + args.newRequest.quantity = item.maxRequestQty; + } else if (args.newRequest.quantity < 1) { + console.log( + "clipping request quantity (too low), original:", + args.newRequest.quantity, + ", new:", + 1 + ); + args.newRequest.quantity = 1; + } + + const initialStatus: RequestStatus = + !item.approvalRequired && item.qtyUnreserved >= args.newRequest.quantity + ? "APPROVED" + : "SUBMITTED"; + + let newRequest: any = await DB.from("requests") + .insert({ + ...args.newRequest, + status: initialStatus, + }) + .returning(["request_id", "quantity", "status", "created_at", "updated_at"]); + + newRequest = newRequest[0]; + const updatedItem = await getItem(args.newRequest.request_item_id, context.user.admin); + if (!updatedItem) { + throw new GraphQLError( + "Unable to retrieve the new item information after creating request" + ); + } + + const simpleRequest = toSimpleRequest(newRequest); + + const result: Request = { + ...simpleRequest, + user, + item: updatedItem, + location: updatedItem.location, + }; + + pubsub.publish(REQUEST_CHANGE, { + [REQUEST_CHANGE]: result, + }); + + return { + request_id: newRequest.request_id, + quantity: args.newRequest.quantity, + status: initialStatus, + item: updatedItem, + location: updatedItem.location, + user, + createdAt: localTimestamp(newRequest.created_at), + updatedAt: localTimestamp(newRequest.updated_at), + }; + }, + deleteRequest: async (root, args, context): Promise => { + if (!context.user.admin) { + throw new GraphQLError("You do not have permission to access the deleteRequest endpoint."); + } + + const numRowsAffected: number = await DB.from("requests") + .where({ + request_id: args.id, + }) + .del(); + + return numRowsAffected !== 0; + }, + updateRequest: async (root, args, context): Promise => { + if (!context.user.admin) { + throw new GraphQLError("You do not have permission to access the updateRequest endpoint."); + } + + const updateObj: any = {}; + + // Not going to validate against maxRequestQty since only admins can change this currently + + const newQuantity: number | undefined = args.updatedRequest.new_quantity; + if (newQuantity && newQuantity <= 0) { + throw new GraphQLError( + `Invalid new requested quantity of ${newQuantity} specified. The new requested quantity must be >= 1.` + ); + } + + // TODO: status change validation logic + if (args.updatedRequest.new_status) { + updateObj.status = args.updatedRequest.new_status; + } + + if (args.updatedRequest.new_quantity) { + updateObj.quantity = args.updatedRequest.new_quantity; + } + + let updatedUserHaveID = null; + if (typeof args.updatedRequest.user_haveID !== "undefined") { + updatedUserHaveID = args.updatedRequest.user_haveID; + } + + if (Object.keys(updateObj).length >= 1) { + updateObj.updated_at = new Date(); + + const updatedRequest: any = await DB.from("requests") + .where({ + request_id: args.updatedRequest.request_id, + }) + .update(updateObj) + .returning([ + "request_id", + "quantity", + "status", + "created_at", + "updated_at", + "user_id", + "request_item_id", + ]); + + const simpleRequest = toSimpleRequest(updatedRequest[0]); + console.log(simpleRequest); + + let user; + if (updatedUserHaveID !== null) { + user = await updateUser( + { + uuid: updatedRequest[0].user_id, + updatedUser: { + haveID: updatedUserHaveID, + }, }, + context + ); + } else { + user = await getUser(updatedRequest[0].user_id); + } + + // fetch the item + const item: Item | null = await getItem( + updatedRequest[0].request_item_id, + context.user.admin + ); + + const location: Location | null = getItemLocation(item); + + if (!item) { + throw new GraphQLError( + `Can't create request for item that doesn't exist! Item ID provided: ${args.newRequest.request_item_id}` + ); } - } + + if (updatedRequest.length === 0) { + return null; + } + + const result: Request = { + ...simpleRequest, + user, + item, + location, + }; + + pubsub.publish(REQUEST_CHANGE, { + [REQUEST_CHANGE]: result, + }); + + return result; + } + + return null; + }, + updateUser: async (root, args, context): Promise => + await updateUser(args, context), + createSetting: async (root, args, context): Promise => { + // Restrict endpoint to admins + if (!context.user.admin) { + throw new GraphQLError("You do not have permission to access the createSetting endpoint"); + } + + if (!args.newSetting.name.trim().length) { + throw new GraphQLError("The setting name (name) can't be empty."); + } + + if (!args.newSetting.value.trim().length) { + throw new GraphQLError("The value for this setting can't be blank."); + } + + const newObjName = await DB.from("settings") + .insert({ + name: args.newSetting.name, + value: args.newSetting.value, + }) + .returning("name"); + + console.log("The new setting's name is", newObjName[0]); + + return { + name: newObjName[0], + value: args.newSetting.value, + }; + }, + /** + * Update an existing setting given its name + * @param root + * @param args + * @param context + */ + updateSetting: async (root, args, context): Promise => { + // Restrict endpoint to admins + if (!context.user.admin) { + throw new GraphQLError("You do not have permission to access the updateSetting endpoint"); + } + + if (!args.name.trim().length) { + throw new GraphQLError("You must provide a valid setting name to update a setting."); + } + + if (!args.updatedSetting.value.trim().length) { + throw new GraphQLError("The value can't be empty."); + } + + await DB.from("settings").where({ name: args.name }).update({ + name: args.updatedSetting.name, + value: args.updatedSetting.value, + }); + + return { + name: args.updatedSetting.name, + value: args.updatedSetting.value, + }; + }, + }, + Subscription: { + request_change: { + subscribe: () => pubsub.asyncIterator(REQUEST_CHANGE), + resolve: payload => payload[REQUEST_CHANGE], + }, + }, }; const schemaFile = path.join(__dirname, "./api.graphql"); export const schema = makeExecutableSchema({ - typeDefs: fs.readFileSync(schemaFile, {encoding: "utf8"}), - resolvers + typeDefs: fs.readFileSync(schemaFile, { encoding: "utf8" }), + resolvers, }); -apiRoutes.post("/", graphqlHTTP({ +apiRoutes.post( + "/", + graphqlHTTP({ schema, - graphiql: false -})); - -apiRoutes.all("/graphiql", isAdminNoAuthCheck, graphqlHTTP({ + graphiql: false, + }) +); + +apiRoutes.all( + "/graphiql", + isAdminNoAuthCheck, + graphqlHTTP({ schema, - graphiql: true -})); + graphiql: true, + }) +); apiRoutes.post("/slack/feedback", bodyParser.json(), (req, res) => { - const user = req.user as User; - - fetch(config.server.slackURL, { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({ - attachments: [ - { - fallback: req.body.fallback, - color: req.body.color, - fields: [ - { - title: "Feedback Type", - value: req.body.title, - }, - { - title: "Comment", - value: req.body.text, - }, - { - title: "URL", - value: req.body.title_link, - }, - { - title: "Name", - value: user.name, - }, - { - title: "UUID", - value: user.uuid, - }, - { - title: "Email", - value: user.email, - }, - { - title: "Slack Username", - value: user.slackUsername, - }, - { - title: "Admin", - value: user.admin ? "Yes" : "No", - } - ] - } - ], - }), - }).then(response => { - return res.status(response.status).send({ - status: response.status, - statusText: response.statusText - }); - }).catch(error => { - return res.status(500).send(error.toString()); - }); + const user = req.user as User; + + fetch(config.server.slackURL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + attachments: [ + { + fallback: req.body.fallback, + color: req.body.color, + fields: [ + { + title: "Feedback Type", + value: req.body.title, + }, + { + title: "Comment", + value: req.body.text, + }, + { + title: "URL", + value: req.body.title_link, + }, + { + title: "Name", + value: user.name, + }, + { + title: "UUID", + value: user.uuid, + }, + { + title: "Email", + value: user.email, + }, + { + title: "Slack Username", + value: user.slackUsername, + }, + { + title: "Admin", + value: user.admin ? "Yes" : "No", + }, + ], + }, + ], + }), + }) + .then(response => + res.status(response.status).send({ + status: response.status, + statusText: response.statusText, + }) + ) + .catch(error => res.status(500).send(error.toString())); }); diff --git a/server/src/api/items/ItemController.ts b/server/src/api/items/ItemController.ts index 7337b54..e5ffaf2 100644 --- a/server/src/api/items/ItemController.ts +++ b/server/src/api/items/ItemController.ts @@ -1,60 +1,58 @@ -import {DB} from "../../database"; -import {Quantity} from "../requests/quantity"; -import {onlyIfAdmin} from "../requests"; -import {Item} from "../graphql.types"; +import { DB } from "../../database"; +import { Quantity } from "../requests/quantity"; +import { onlyIfAdmin } from "../requests"; +import { Item } from "../graphql.types"; export class ItemController { - public static async getTotalAvailable(itemIds: number[] = []) { - const result = await DB.from("items") - .where((builder) => { - if (itemIds.length) { - builder.whereIn("item_id", itemIds); - } - }) - .select(["item_id", "totalAvailable"]); - - if (!result.length) { - return {}; + public static async getTotalAvailable(itemIds: number[] = []) { + const result = await DB.from("items") + .where(builder => { + if (itemIds.length) { + builder.whereIn("item_id", itemIds); } + }) + .select(["item_id", "totalAvailable"]); - const resultObj = {}; + if (!result.length) { + return {}; + } - for (let i = 0; i < result.length; i++) { - const item = result[i]; - resultObj[item.item_id] = item.totalAvailable; - } + const resultObj = {}; - return resultObj; + for (let i = 0; i < result.length; i++) { + const item = result[i]; + resultObj[item.item_id] = item.totalAvailable; } - public static async get(searchObj: any, isAdmin: boolean): Promise { - const items = await DB.from("items") - .where(searchObj) - .join("categories", "items.category_id", "=", "categories.category_id") - .join("locations", "locations.location_id", "=", "items.location_id"); - - const {qtyInStock, qtyUnreserved, qtyAvailableForApproval} = await Quantity.all(); - - return items.map(item => { - return { - ...item, - id: item.item_id, - category: item.category_name, - location: getItemLocation(item), - price: onlyIfAdmin(item.price, isAdmin), - owner: onlyIfAdmin(item.owner, isAdmin), - qtyInStock: qtyInStock[item.item_id], - qtyUnreserved: qtyUnreserved[item.item_id], - qtyAvailableForApproval: qtyAvailableForApproval[item.item_id], - }; - }); - } + return resultObj; + } + + public static async get(searchObj: any, isAdmin: boolean): Promise { + const items = await DB.from("items") + .where(searchObj) + .join("categories", "items.category_id", "=", "categories.category_id") + .join("locations", "locations.location_id", "=", "items.location_id"); + + const { qtyInStock, qtyUnreserved, qtyAvailableForApproval } = await Quantity.all(); + + return items.map(item => ({ + ...item, + id: item.item_id, + category: item.category_name, + location: getItemLocation(item), + price: onlyIfAdmin(item.price, isAdmin), + owner: onlyIfAdmin(item.owner, isAdmin), + qtyInStock: qtyInStock[item.item_id], + qtyUnreserved: qtyUnreserved[item.item_id], + qtyAvailableForApproval: qtyAvailableForApproval[item.item_id], + })); + } } export function getItemLocation(item: any) { - return { - location_id: item.location_id, - location_name: item.location_name, - location_hidden: item.location_hidden - }; + return { + location_id: item.location_id, + location_name: item.location_name, + location_hidden: item.location_hidden, + }; } diff --git a/server/src/api/requests.ts b/server/src/api/requests.ts index 5dac0cb..9de5b50 100644 --- a/server/src/api/requests.ts +++ b/server/src/api/requests.ts @@ -1,44 +1,44 @@ import moment from "moment"; -import {RequestStatus} from "./graphql.types"; -import {ItemQtyAvailable} from "./requests/quantity"; + +import { RequestStatus } from "./graphql.types"; +import { ItemQtyAvailable } from "./requests/quantity"; export interface KnexSimpleRequest { - request_id: number; - status: RequestStatus; - quantity: number; - created_at: string; - updated_at: string; + request_id: number; + status: RequestStatus; + quantity: number; + created_at: string; + updated_at: string; } export interface KnexRequest extends KnexSimpleRequest { - item_id: number; - item_name: string; - description: string; - imageUrl: string; - category_id: number; - category_name: string; - totalAvailable: number; - maxRequestQty: number; - price: number; - hidden: boolean; - returnRequired: boolean; - approvalRequired: boolean; - owner: string; - uuid: string; - admin: boolean; - name: string; - email: string; - phone: string; - slackUsername: string; - haveID: boolean; - qtyInStock: number; - qtyUnreserved: number; - qtyAvailableForApproval: number; + item_id: number; + item_name: string; + description: string; + imageUrl: string; + category_id: number; + category_name: string; + totalAvailable: number; + maxRequestQty: number; + price: number; + hidden: boolean; + returnRequired: boolean; + approvalRequired: boolean; + owner: string; + uuid: string; + admin: boolean; + name: string; + email: string; + phone: string; + slackUsername: string; + haveID: boolean; + qtyInStock: number; + qtyUnreserved: number; + qtyAvailableForApproval: number; } - export function localTimestamp(createdAt: string): string { - return moment(createdAt).toISOString(true); + return moment(createdAt).toISOString(true); } /** @@ -47,63 +47,76 @@ export function localTimestamp(createdAt: string): string { * @param isAdmin */ export function onlyIfAdmin(val: any, isAdmin: boolean) { - return (isAdmin) ? val : null; + return isAdmin ? val : null; } -export function nestedRequest(request: KnexRequest, isAdmin: boolean, qtyInStock: ItemQtyAvailable, qtyAvailableForApproval: ItemQtyAvailable, qtyUnreserved: ItemQtyAvailable) { - const user = { - uuid: request.uuid, - admin: request.admin, - name: request.name, - email: request.email, - phone: request.phone, - slackUsername: request.slackUsername, - haveID: request.haveID - }; +export function nestedRequest( + request: KnexRequest, + isAdmin: boolean, + qtyInStock: ItemQtyAvailable, + qtyAvailableForApproval: ItemQtyAvailable, + qtyUnreserved: ItemQtyAvailable +) { + const user = { + uuid: request.uuid, + admin: request.admin, + name: request.name, + email: request.email, + phone: request.phone, + slackUsername: request.slackUsername, + haveID: request.haveID, + }; - return { - user, - item: redactedItem(request, isAdmin, qtyInStock, qtyAvailableForApproval, qtyUnreserved), - request_id: request.request_id, - status: request.status, - location: redactedItem(request, isAdmin, qtyInStock, qtyAvailableForApproval, qtyUnreserved).location, - quantity: request.quantity, - createdAt: localTimestamp(request.created_at), - updatedAt: localTimestamp(request.updated_at) - }; + return { + user, + item: redactedItem(request, isAdmin, qtyInStock, qtyAvailableForApproval, qtyUnreserved), + request_id: request.request_id, + status: request.status, + location: redactedItem(request, isAdmin, qtyInStock, qtyAvailableForApproval, qtyUnreserved) + .location, + quantity: request.quantity, + createdAt: localTimestamp(request.created_at), + updatedAt: localTimestamp(request.updated_at), + }; } -export function redactedItem(item: any, isAdmin: boolean, qtyInStock: ItemQtyAvailable, qtyAvailableForApproval: ItemQtyAvailable, qtyUnreserved: ItemQtyAvailable) { - return { - id: item.item_id, - item_name: item.item_name, - description: item.description, - imageUrl: item.imageUrl, - category: item.category_name, - location: { - location_id: item.location_id, - location_name: item.location_name, - location_hidden: item.location_hidden - }, - totalAvailable: item.totalAvailable, - maxRequestQty: item.maxRequestQty, - price: onlyIfAdmin(item.price, isAdmin), - hidden: item.hidden, - returnRequired: item.returnRequired, - approvalRequired: item.approvalRequired, - owner: onlyIfAdmin(item.owner, isAdmin), - qtyInStock: qtyInStock[item.item_id], - qtyAvailableForApproval: qtyAvailableForApproval[item.item_id], - qtyUnreserved: qtyUnreserved[item.item_id] - }; +export function redactedItem( + item: any, + isAdmin: boolean, + qtyInStock: ItemQtyAvailable, + qtyAvailableForApproval: ItemQtyAvailable, + qtyUnreserved: ItemQtyAvailable +) { + return { + id: item.item_id, + item_name: item.item_name, + description: item.description, + imageUrl: item.imageUrl, + category: item.category_name, + location: { + location_id: item.location_id, + location_name: item.location_name, + location_hidden: item.location_hidden, + }, + totalAvailable: item.totalAvailable, + maxRequestQty: item.maxRequestQty, + price: onlyIfAdmin(item.price, isAdmin), + hidden: item.hidden, + returnRequired: item.returnRequired, + approvalRequired: item.approvalRequired, + owner: onlyIfAdmin(item.owner, isAdmin), + qtyInStock: qtyInStock[item.item_id], + qtyAvailableForApproval: qtyAvailableForApproval[item.item_id], + qtyUnreserved: qtyUnreserved[item.item_id], + }; } export function toSimpleRequest(request: KnexSimpleRequest) { - return { - request_id: request.request_id, - status: request.status, - quantity: request.quantity, - createdAt: localTimestamp(request.created_at), - updatedAt: localTimestamp(request.updated_at) - }; + return { + request_id: request.request_id, + status: request.status, + quantity: request.quantity, + createdAt: localTimestamp(request.created_at), + updatedAt: localTimestamp(request.updated_at), + }; } diff --git a/server/src/api/requests/quantity.ts b/server/src/api/requests/quantity.ts index 125a088..95989b2 100644 --- a/server/src/api/requests/quantity.ts +++ b/server/src/api/requests/quantity.ts @@ -1,133 +1,184 @@ -import {RequestStatus} from "../graphql.types"; -import {DB} from "../../database"; -import {ItemController} from "../items/ItemController"; +import { RequestStatus } from "../graphql.types"; +import { DB } from "../../database"; +import { ItemController } from "../items/ItemController"; interface QuantitiesInStatus { - [statusName: string]: number; + [statusName: string]: number; } interface ItemQuantities { - [itemId: string]: QuantitiesInStatus; + [itemId: string]: QuantitiesInStatus; } export interface ItemQtyAvailable { - [itemId: string]: number; + [itemId: string]: number; } export interface ItemAllQtys { - qtyInStock: ItemQtyAvailable; - qtyUnreserved: ItemQtyAvailable; - qtyAvailableForApproval: ItemQtyAvailable; + qtyInStock: ItemQtyAvailable; + qtyUnreserved: ItemQtyAvailable; + qtyAvailableForApproval: ItemQtyAvailable; } export class Quantity { - public static async inStock(itemIds: number[] = []): Promise { - const quantities: ItemQuantities = await Quantity.getQuantities(["SUBMITTED", "APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"], itemIds); - const totalAvailable: ItemQtyAvailable = await ItemController.getTotalAvailable(itemIds); - - return this.getTotalAvailableLessStatuses(quantities, totalAvailable, ["FULFILLED", "LOST", "DAMAGED"]); + public static async inStock(itemIds: number[] = []): Promise { + const quantities: ItemQuantities = await Quantity.getQuantities( + ["SUBMITTED", "APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"], + itemIds + ); + const totalAvailable: ItemQtyAvailable = await ItemController.getTotalAvailable(itemIds); + + return this.getTotalAvailableLessStatuses(quantities, totalAvailable, [ + "FULFILLED", + "LOST", + "DAMAGED", + ]); + } + + public static async unreserved(itemIds: number[] = []): Promise { + const quantities: ItemQuantities = await Quantity.getQuantities( + ["SUBMITTED", "APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"], + itemIds + ); + const totalAvailable: ItemQtyAvailable = await ItemController.getTotalAvailable(itemIds); + + return this.getTotalAvailableLessStatuses(quantities, totalAvailable, [ + "SUBMITTED", + "APPROVED", + "READY_FOR_PICKUP", + "FULFILLED", + "LOST", + "DAMAGED", + ]); + } + + public static async availableForApproval(itemIds: number[] = []): Promise { + const quantities: ItemQuantities = await Quantity.getQuantities( + ["SUBMITTED", "APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"], + itemIds + ); + const totalAvailable: ItemQtyAvailable = await ItemController.getTotalAvailable(itemIds); + + return this.getTotalAvailableLessStatuses(quantities, totalAvailable, [ + "APPROVED", + "READY_FOR_PICKUP", + "FULFILLED", + "LOST", + "DAMAGED", + ]); + } + + public static async all(itemIds: number[] = []): Promise { + const quantities: ItemQuantities = await Quantity.getQuantities( + ["SUBMITTED", "APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"], + itemIds + ); + const totalAvailable: ItemQtyAvailable = await ItemController.getTotalAvailable(itemIds); + + const qtyInStock: ItemQtyAvailable = this.getTotalAvailableLessStatuses( + quantities, + totalAvailable, + ["FULFILLED", "LOST", "DAMAGED"] + ); + const qtyUnreserved: ItemQtyAvailable = this.getTotalAvailableLessStatuses( + quantities, + totalAvailable, + ["SUBMITTED", "APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"] + ); + const qtyAvailableForApproval: ItemQtyAvailable = this.getTotalAvailableLessStatuses( + quantities, + totalAvailable, + ["APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"] + ); + + return { + qtyInStock, + qtyUnreserved, + qtyAvailableForApproval, + }; + } + + public static async quantityStatistics(itemIds: number[] = []): Promise { + return await Quantity.getQuantities(); + } + + private static getTotalAvailableLessStatuses( + quantities: ItemQuantities, + totalAvailable: ItemQtyAvailable, + statuses: RequestStatus[] = [] + ): ItemQtyAvailable { + const result = {}; + + for (const id in totalAvailable) { + if (totalAvailable.hasOwnProperty(id)) { + if (quantities.hasOwnProperty(id)) { + const itemStatusCounts = quantities[id]; + let quantity = totalAvailable[id]; + for (let i = 0; i < statuses.length; i++) { + quantity -= itemStatusCounts[statuses[i]]; + } + result[id] = quantity; + } else { + // no requests for this item with statuses provided, so just return totalAvailable + result[id] = totalAvailable[id]; + } + } } - public static async unreserved(itemIds: number[] = []): Promise { - const quantities: ItemQuantities = await Quantity.getQuantities(["SUBMITTED", "APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"], itemIds); - const totalAvailable: ItemQtyAvailable = await ItemController.getTotalAvailable(itemIds); - - return this.getTotalAvailableLessStatuses(quantities, totalAvailable, ["SUBMITTED", "APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"]); + return result; + } + + private static async getQuantities( + statuses: RequestStatus[] = [], + itemIds: number[] = [] + ): Promise { + if (!statuses.length) { + statuses = [ + "SUBMITTED", + "APPROVED", + "DENIED", + "ABANDONED", + "CANCELLED", + "READY_FOR_PICKUP", + "FULFILLED", + "RETURNED", + "LOST", + "DAMAGED", + ]; } - public static async availableForApproval(itemIds: number[] = []): Promise { - const quantities: ItemQuantities = await Quantity.getQuantities(["SUBMITTED", "APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"], itemIds); - const totalAvailable: ItemQtyAvailable = await ItemController.getTotalAvailable(itemIds); - - return this.getTotalAvailableLessStatuses(quantities, totalAvailable, ["APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"]); - } - - public static async all(itemIds: number[] = []): Promise { - const quantities: ItemQuantities = await Quantity.getQuantities(["SUBMITTED", "APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"], itemIds); - const totalAvailable: ItemQtyAvailable = await ItemController.getTotalAvailable(itemIds); + const DBQuantities: any = await DB.from("requests") + .where(builder => { + const whereInStatus = builder.whereIn("status", statuses); + if (itemIds.length > 0) { + return whereInStatus.whereIn("request_item_id", itemIds); + } - const qtyInStock: ItemQtyAvailable = this.getTotalAvailableLessStatuses(quantities, totalAvailable, ["FULFILLED", "LOST", "DAMAGED"]); - const qtyUnreserved: ItemQtyAvailable = this.getTotalAvailableLessStatuses(quantities, totalAvailable, ["SUBMITTED", "APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"]); - const qtyAvailableForApproval: ItemQtyAvailable = this.getTotalAvailableLessStatuses(quantities, totalAvailable, ["APPROVED", "READY_FOR_PICKUP", "FULFILLED", "LOST", "DAMAGED"]); + return whereInStatus; + }) + .groupBy(["request_item_id", "status"]) + .select(["request_item_id", "status"]) + .sum("quantity"); - return { - qtyInStock, - qtyUnreserved, - qtyAvailableForApproval - }; + const baseObj = {}; + for (let i = 0; i < statuses.length; i++) { + baseObj[statuses[i]] = 0; } - public static async quantityStatistics(itemIds: number[] = []): Promise { - return await Quantity.getQuantities(); - } + const result = {}; + for (let i = 0; i < DBQuantities.length; i++) { + const item = DBQuantities[i]; + const requestItemId = item.request_item_id; - private static getTotalAvailableLessStatuses(quantities: ItemQuantities, totalAvailable: ItemQtyAvailable, statuses: RequestStatus[] = []): ItemQtyAvailable { - const result = {}; - - for (const id in totalAvailable) { - if (totalAvailable.hasOwnProperty(id)) { - if (quantities.hasOwnProperty(id)) { - const itemStatusCounts = quantities[id]; - let quantity = totalAvailable[id]; - for (let i = 0; i < statuses.length; i++) { - quantity -= itemStatusCounts[statuses[i]]; - } - result[id] = quantity; - } else { // no requests for this item with statuses provided, so just return totalAvailable - result[id] = totalAvailable[id]; - } - } - } + if (!result.hasOwnProperty(requestItemId.toString(10))) { + result[requestItemId] = { ...baseObj }; // make a copy of baseObj + } - return result; + result[requestItemId][item.status] = Number.parseInt(item.sum, 10); + result[requestItemId].total = + result[requestItemId][item.status] + (result[requestItemId].total || 0); } - private static async getQuantities(statuses: RequestStatus[] = [], itemIds: number[] = []): Promise { - if (!statuses.length) { - statuses = ["SUBMITTED", - "APPROVED", - "DENIED", - "ABANDONED", - "CANCELLED", - "READY_FOR_PICKUP", - "FULFILLED", - "RETURNED", - "LOST", - "DAMAGED"]; - } - - const DBQuantities: any = await DB.from("requests") - .where((builder) => { - const whereInStatus = builder.whereIn("status", statuses); - if (itemIds.length > 0) { - return whereInStatus.whereIn("request_item_id", itemIds); - } - - return whereInStatus; - } - ) - .groupBy(["request_item_id", "status"]) - .select(["request_item_id", "status"]) - .sum("quantity"); - - const baseObj = {}; - for (let i = 0; i < statuses.length; i++) { - baseObj[statuses[i]] = 0; - } - - const result = {}; - for (let i = 0; i < DBQuantities.length; i++) { - const item = DBQuantities[i]; - const requestItemId = item.request_item_id; - - if (!result.hasOwnProperty(requestItemId.toString(10))) { - result[requestItemId] = Object.assign({}, baseObj); // make a copy of baseObj - } - - result[requestItemId][item.status] = Number.parseInt(item.sum, 10); - result[requestItemId].total = result[requestItemId][item.status] + (result[requestItemId].total || 0); - } - - return result; - } + return result; + } } diff --git a/server/src/app.ts b/server/src/app.ts index 04953c7..19555a2 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1,5 +1,4 @@ import * as path from "path"; - import express from "express"; import serveStatic from "serve-static"; import compression from "compression"; @@ -7,133 +6,144 @@ import cookieParser from "cookie-parser"; import * as cookieSignature from "cookie-signature"; import * as chalk from "chalk"; import morgan from "morgan"; -import {config, COOKIE_OPTIONS, PORT, VERSION_NUMBER} from "./common"; +import { execute, subscribe } from "graphql"; +import { SubscriptionServer } from "subscriptions-transport-ws"; +import { createServer } from "http"; +import { config, COOKIE_OPTIONS, PORT, VERSION_NUMBER } from "./common"; + +import { findUserByID } from "./database"; + +// *** The placement of these imports is very important; ensure that your editor does not optimize the imports or otherwise +// reformat this file as it will cause errors (most likely similar to +// "/usr/src/bolt/server/build/auth/auth.js:37 +// app_1.app.enable("trust proxy"); +// TypeError: Cannot read property 'enable' of undefined") +// if they are moved to the top of this file +// Auth needs to be the first route configured or else requests handled before it will always be unauthenticated +import { authRoutes, isAuthenticated, sessionMiddleware } from "./auth/auth"; + +// *** The placement of this import is also important! (See above) +import { apiRoutes, schema } from "./api/api"; -import {execute, subscribe} from "graphql"; -import {SubscriptionServer} from "subscriptions-transport-ws"; -import {createServer} from "http"; -import {findUserByID} from "./database"; import flash = require("connect-flash"); // Set up Express and its middleware -export let app = express(); +export const app = express(); app.use(compression()); -const cookieParserInstance = cookieParser(undefined, COOKIE_OPTIONS as cookieParser.CookieParseOptions); +const cookieParserInstance = cookieParser( + undefined, + COOKIE_OPTIONS as cookieParser.CookieParseOptions +); app.use(cookieParserInstance); morgan.token("sessionid", (request, response) => { - const FAILURE_MESSAGE = "Unknown session"; - // @ts-ignore - if (!request.cookies["connect.sid"]) { - return FAILURE_MESSAGE; - } - // @ts-ignore - const rawID: string = request.cookies["connect.sid"].slice(2); - const id = cookieSignature.unsign(rawID, config.secrets.session); - if (typeof id === "string") { - return id; - } + const FAILURE_MESSAGE = "Unknown session"; + // @ts-ignore + if (!request.cookies["connect.sid"]) { return FAILURE_MESSAGE; + } + // @ts-ignore + const rawID: string = request.cookies["connect.sid"].slice(2); + const id = cookieSignature.unsign(rawID, config.secrets.session); + if (typeof id === "string") { + return id; + } + return FAILURE_MESSAGE; }); morgan.format("hackgt", (tokens, request, response) => { - let statusColorizer: (input: string) => string = input => input; // Default passthrough function - if (response.statusCode >= 500) { - statusColorizer = chalk.default.red; - } else if (response.statusCode >= 400) { - statusColorizer = chalk.default.yellow; - } else if (response.statusCode >= 300) { - statusColorizer = chalk.default.cyan; - } else if (response.statusCode >= 200) { - statusColorizer = chalk.default.green; - } - - return [ - tokens.date(request, response, "iso"), - tokens["remote-addr"](request, response), - tokens.sessionid(request, response), - tokens.method(request, response), - tokens.url(request, response), - statusColorizer(tokens.status(request, response)!!), - tokens["response-time"](request, response), "ms", "-", - tokens.res(request, response, "content-length") - ].join(" "); + let statusColorizer: (input: string) => string = input => input; // Default passthrough function + if (response.statusCode >= 500) { + statusColorizer = chalk.default.red; + } else if (response.statusCode >= 400) { + statusColorizer = chalk.default.yellow; + } else if (response.statusCode >= 300) { + statusColorizer = chalk.default.cyan; + } else if (response.statusCode >= 200) { + statusColorizer = chalk.default.green; + } + + return [ + tokens.date(request, response, "iso"), + tokens["remote-addr"](request, response), + tokens.sessionid(request, response), + tokens.method(request, response), + tokens.url(request, response), + statusColorizer(tokens.status(request, response)!), + tokens["response-time"](request, response), + "ms", + "-", + tokens.res(request, response, "content-length"), + ].join(" "); }); app.use(morgan("hackgt")); app.use(flash()); // Throw and show a stack trace on an unhandled Promise rejection instead of logging an unhelpful warning process.on("unhandledRejection", err => { - throw err; + throw err; }); - -// *** The placement of these imports is very important; ensure that your editor does not optimize the imports or otherwise -// reformat this file as it will cause errors (most likely similar to -// "/usr/src/bolt/server/build/auth/auth.js:37 -// app_1.app.enable("trust proxy"); -// TypeError: Cannot read property 'enable' of undefined") -// if they are moved to the top of this file -// Auth needs to be the first route configured or else requests handled before it will always be unauthenticated -import {authRoutes, isAuthenticated, sessionMiddleware} from "./auth/auth"; app.use("/auth", authRoutes); - -// *** The placement of this import is also important! (See above) -import {apiRoutes, schema} from "./api/api"; app.use("/api", isAuthenticated, apiRoutes); app.route("/version").get((request, response) => { - response.json({ - version: VERSION_NUMBER, - node: process.version - }); + response.json({ + version: VERSION_NUMBER, + node: process.version, + }); }); // Serve React app app.use(isAuthenticated, serveStatic(path.join(__dirname, "../../client/build"))); - app.get("*", isAuthenticated, (request, response) => { - response.sendFile(path.join(__dirname, "../../client/build", "index.html")); + response.sendFile(path.join(__dirname, "../../client/build", "index.html")); }); - const server = createServer(app); - server.listen(PORT, () => { - console.log(`Bolt v${VERSION_NUMBER} started on port ${PORT}`); - - // tslint:disable-next-line:no-unused-expression - new SubscriptionServer({ - execute, - subscribe, - schema, - onConnect: async (connectParams, webSocket, context) => { - - const promise = new Promise((resolve, reject) => { - // session is the Express library that creates the sessionMiddleware function that parses the session - // cookie. - // sessionMiddleware is the actual function to call to parse a session cookie. That's essentially - // what's happening below. - - // @ts-ignore - sessionMiddleware(webSocket.upgradeReq, {}, () => { - return resolve(webSocket.upgradeReq.session.passport); - }); - }); - - const {user}: any = await promise; - const fullUser = await findUserByID(user); - - if (fullUser && fullUser.admin) { - return true; - } - - console.log("Rejecting websocket connection: user (", user, ") is not an admin, fullUser:", fullUser); - throw new Error("Websocket connection rejected: your account does not have permission access this endpoint"); - } - }, - { - server, - path: "/api" + console.log(`Bolt v${VERSION_NUMBER} started on port ${PORT}`); + + // tslint:disable-next-line:no-unused-expression + new SubscriptionServer( + { + execute, + subscribe, + schema, + onConnect: async (connectParams, webSocket, context) => { + const promise = new Promise((resolve, reject) => { + // session is the Express library that creates the sessionMiddleware function that parses the session + // cookie. + // sessionMiddleware is the actual function to call to parse a session cookie. That's essentially + // what's happening below. + + // @ts-ignore + sessionMiddleware(webSocket.upgradeReq, {}, () => + resolve(webSocket.upgradeReq.session.passport) + ); }); + + const { user }: any = await promise; + const fullUser = await findUserByID(user); + + if (fullUser && fullUser.admin) { + return true; + } + + console.log( + "Rejecting websocket connection: user (", + user, + ") is not an admin, fullUser:", + fullUser + ); + throw new Error( + "Websocket connection rejected: your account does not have permission access this endpoint" + ); + }, + }, + { + server, + path: "/api", + } + ); }); diff --git a/server/src/auth/auth.ts b/server/src/auth/auth.ts index 1f1934f..22ab366 100644 --- a/server/src/auth/auth.ts +++ b/server/src/auth/auth.ts @@ -1,109 +1,129 @@ import * as http from "http"; import * as https from "https"; -import {URL} from "url"; +import { URL } from "url"; import * as crypto from "crypto"; import express from "express"; import session from "express-session"; import pgSession from "connect-pg-simple"; import passport from "passport"; -import {findUserByID, IUser} from "../database"; - -import {config, COOKIE_OPTIONS} from "../common"; - -import {AuthenticateOptions, createLink, GroundTruthStrategy, validateAndCacheHostName} from "./strategies"; +import { findUserByID, IUser } from "../database"; +import { config, COOKIE_OPTIONS } from "../common"; +import { + AuthenticateOptions, + createLink, + GroundTruthStrategy, + validateAndCacheHostName, +} from "./strategies"; // Passport authentication -import {app} from "../app"; +import { app } from "../app"; if (!config.server.isProduction) { - console.warn("OAuth callback(s) running in development mode"); + console.warn("OAuth callback(s) running in development mode"); } else { - app.enable("trust proxy"); + app.enable("trust proxy"); } if (!config.sessionSecretSet) { - console.warn("No session secret set; sessions won't carry over server restarts"); + console.warn("No session secret set; sessions won't carry over server restarts"); } export const sessionMiddleware = session({ - secret: config.secrets.session, - cookie: COOKIE_OPTIONS, - resave: false, - store: new (pgSession(session))({ - conString: config.server.postgresURL - }), - saveUninitialized: false + secret: config.secrets.session, + cookie: COOKIE_OPTIONS, + resave: false, + store: new (pgSession(session))({ + conString: config.server.postgresURL, + }), + saveUninitialized: false, }); app.use(sessionMiddleware); // @ts-ignore passport.serializeUser((user, done) => { - // @ts-ignore - done(null, user.uuid); + // @ts-ignore + done(null, user.uuid); }); // @ts-ignore passport.deserializeUser(async (id, done) => { - // @ts-ignore - findUserByID(id).then(user => { - done(null, user!); - }).catch(err => { - done(err); + // @ts-ignore + findUserByID(id) + .then(user => { + done(null, user!); + }) + .catch(err => { + done(err); }); }); -export function isAuthenticated(request: express.Request, response: express.Response, next: express.NextFunction): void { - response.setHeader("Cache-Control", "private"); - if (!request.isAuthenticated() || !request.user) { - if (request.session) { - // @ts-ignore - request.session.returnTo = request.originalUrl; - } - response.redirect("/auth/login"); - } else { - next(); +export function isAuthenticated( + request: express.Request, + response: express.Response, + next: express.NextFunction +): void { + response.setHeader("Cache-Control", "private"); + if (!request.isAuthenticated() || !request.user) { + if (request.session) { + // @ts-ignore + request.session.returnTo = request.originalUrl; } + response.redirect("/auth/login"); + } else { + next(); + } } -export function isAdmin(request: express.Request, response: express.Response, next: express.NextFunction): void { - isAuthenticated(request, response, next); - isAdminNoAuthCheck(request, response, next); +export function isAdmin( + request: express.Request, + response: express.Response, + next: express.NextFunction +): void { + isAuthenticated(request, response, next); + isAdminNoAuthCheck(request, response, next); } - -export function isAdminNoAuthCheck(request: express.Request, response: express.Response, next: express.NextFunction): void { - // @ts-ignore - if (request.user && request.user.admin) { - next(); - return; // Prevents a "Can't set headers after they are sent" error - } - response.status(403).send("You are not permitted to access this endpoint"); +export function isAdminNoAuthCheck( + request: express.Request, + response: express.Response, + next: express.NextFunction +): void { + // @ts-ignore + if (request.user && request.user.admin) { + next(); + return; // Prevents a "Can't set headers after they are sent" error + } + response.status(403).send("You are not permitted to access this endpoint"); } -export let authRoutes = express.Router(); +export const authRoutes = express.Router(); authRoutes.get("/validatehost/:nonce", (request, response) => { - const nonce: string = request.params.nonce || ""; - response.send(crypto.createHmac("sha256", config.secrets.session).update(nonce).digest().toString("hex")); + const nonce: string = request.params.nonce || ""; + response.send( + crypto.createHmac("sha256", config.secrets.session).update(nonce).digest().toString("hex") + ); }); authRoutes.all("/logout", (request, response) => { - const user = request.user as IUser | undefined; - if (user) { - const groundTruthURL = new URL(config.secrets.groundTruth.url); - const requester = groundTruthURL.protocol === "http:" ? http : https; - requester.request(new URL("/api/user/logout", config.secrets.groundTruth.url), { - method: "POST", - headers: { - Authorization: `Bearer ${user.token}` - } - }).end(); - - request.logout(); - } - if (request.session) { - // @ts-ignore - request.session.loginAction = "render"; - } - response.redirect("/auth/login"); + const user = request.user as IUser | undefined; + if (user) { + const groundTruthURL = new URL(config.secrets.groundTruth.url); + const requester = groundTruthURL.protocol === "http:" ? http : https; + requester + .request(new URL("/api/user/logout", config.secrets.groundTruth.url), { + method: "POST", + headers: { + Authorization: `Bearer ${user.token}`, + }, + }) + .end(); + + request.logout(); + } + if (request.session) { + // @ts-ignore + request.session.loginAction = "render"; + } + response.redirect("/auth/login"); }); app.use(passport.initialize()); @@ -114,23 +134,23 @@ const groundTruthStrategy = new GroundTruthStrategy(config.secrets.groundTruth.u passport.use(groundTruthStrategy); authRoutes.get("/login", validateAndCacheHostName, (request, response, next) => { - const callbackURL = createLink(request, "auth/login/callback"); - passport.authenticate("oauth2", { callbackURL } as AuthenticateOptions)(request, response, next); + const callbackURL = createLink(request, "auth/login/callback"); + passport.authenticate("oauth2", { callbackURL } as AuthenticateOptions)(request, response, next); }); authRoutes.get("/failure", (request, response) => { - response.send(request.flash("error")); + response.send(request.flash("error")); }); authRoutes.get("/login/callback", validateAndCacheHostName, (request, response, next) => { - const callbackURL = createLink(request, "auth/login/callback"); + const callbackURL = createLink(request, "auth/login/callback"); - if (request.query.error === "access_denied") { - request.flash("error", "Authentication request was denied"); - response.redirect("/auth/login"); - return; - } - passport.authenticate("oauth2", { - failureRedirect: "/auth/failure", - successReturnToOrRedirect: "/", - callbackURL - } as AuthenticateOptions)(request, response, next); + if (request.query.error === "access_denied") { + request.flash("error", "Authentication request was denied"); + response.redirect("/auth/login"); + return; + } + passport.authenticate("oauth2", { + failureRedirect: "/auth/failure", + successReturnToOrRedirect: "/", + callbackURL, + } as AuthenticateOptions)(request, response, next); }); diff --git a/server/src/auth/strategies.ts b/server/src/auth/strategies.ts index caed7fa..2cae98f 100644 --- a/server/src/auth/strategies.ts +++ b/server/src/auth/strategies.ts @@ -1,197 +1,226 @@ import * as crypto from "crypto"; import * as http from "http"; import * as https from "https"; -import {URL} from "url"; +import { URL } from "url"; import * as passport from "passport"; -import {Strategy as OAuthStrategy} from "passport-oauth2"; +import { Strategy as OAuthStrategy } from "passport-oauth2"; +import { NextFunction, Request, Response } from "express"; -import {createRecord, DB, findUserByID, IUser} from "../database"; -import {config} from "../common"; -import {NextFunction, Request, Response} from "express"; +import { createRecord, DB, findUserByID, IUser } from "../database"; +import { config } from "../common"; -type PassportDone = (err: Error | null, user?: IUser | false, errMessage?: { message: string }) => void; +type PassportDone = ( + err: Error | null, + user?: IUser | false, + errMessage?: { message: string } +) => void; type PassportProfileDone = (err: Error | null, profile?: IProfile) => void; interface IStrategyOptions { - passReqToCallback: true; // Forced to true for our usecase + passReqToCallback: true; // Forced to true for our usecase } interface IOAuthStrategyOptions extends IStrategyOptions { - authorizationURL: string; - tokenURL: string; - clientID: string; - clientSecret: string; - scope: string[]; + authorizationURL: string; + tokenURL: string; + clientID: string; + clientSecret: string; + scope: string[]; } interface IProfile { - uuid: string; - name: string; - nameParts?: { firstName: string, lastName: string, preferredName?: string }; - email: string; - token: string; - scopes?: IProfileScopes | null; - member?: boolean; + uuid: string; + name: string; + nameParts?: { firstName: string; lastName: string; preferredName?: string }; + email: string; + token: string; + scopes?: IProfileScopes | null; + member?: boolean; } interface IProfileScopes { - slack: string; - phone: string; + slack: string; + phone: string; } // Because the passport typedefs don't include this for some reason // Defined: // https://github.com/jaredhanson/passport-oauth2/blob/9ddff909a992c3428781b7b2957ce1a97a924367/lib/strategy.js#L135 export type AuthenticateOptions = passport.AuthenticateOptions & { - callbackURL: string; + callbackURL: string; }; export class GroundTruthStrategy extends OAuthStrategy { + public static get defaultUserProperties(): { admin: boolean; haveID: boolean } { + return { + admin: false, + haveID: false, + }; + } + + protected static async passportCallback( + request: Request, + accessToken: string, + refreshToken: string, + profile: IProfile, + done: PassportDone + ): Promise { + let user = await findUserByID(profile.uuid); + if (!user) { + console.log(profile); + const { scopes } = profile; + if (!scopes) { + console.warn(`User ${profile.uuid} has no scope data`); + done(new Error("No scope data for new user")); + return; + } + delete profile.scopes; + delete profile.nameParts; // Basically ignore the Ground Truth nameParts field for now + const adminBecauseHackGTMember = profile.member || false; + delete profile.member; + user = await createRecord("users", { + ...GroundTruthStrategy.defaultUserProperties, + ...profile, + slackUsername: scopes.slack, + phone: scopes.phone, + admin: adminBecauseHackGTMember, + }); + } else { + user.token = accessToken; - public static get defaultUserProperties(): { admin: boolean, haveID: boolean } { - return { - admin: false, - haveID: false - }; + if (profile.member) { + user.admin = true; + } } - protected static async passportCallback(request: Request, accessToken: string, refreshToken: string, - profile: IProfile, done: PassportDone): Promise { - let user = await findUserByID(profile.uuid); - if (!user) { - console.log(profile); - const scopes = profile.scopes; - if (!scopes) { - console.warn(`User ${profile.uuid} has no scope data`); - done(new Error("No scope data for new user")); - return; - } - delete profile.scopes; - delete profile.nameParts; // Basically ignore the Ground Truth nameParts field for now - const adminBecauseHackGTMember = profile.member || false; - delete profile.member; - user = await createRecord("users", { - ...GroundTruthStrategy.defaultUserProperties, - ...profile, - slackUsername: scopes.slack, - phone: scopes.phone, - admin: adminBecauseHackGTMember - }); - } else { - user.token = accessToken; - - if (profile.member) { - user.admin = true; - } - } - - const domain = user.email.split("@").pop(); - if (domain && config.admins.domains.includes(domain)) { - user.admin = true; - } - if (config.admins.emails.includes(profile.email)) { - user.admin = true; - } - await DB.from("users").where({ uuid: user.uuid }).update(user); - done(null, user); + const domain = user.email.split("@").pop(); + if (domain && config.admins.domains.includes(domain)) { + user.admin = true; } - public readonly url: string; - - constructor(url: string) { - const secrets = config.secrets.groundTruth; - if (!secrets || !secrets.id || !secrets.secret) { - throw new Error(`Client ID or secret not configured in config.json or environment variables for Ground - Truth`); - } - const options: IOAuthStrategyOptions = { - authorizationURL: new URL("/oauth/authorize", url).toString(), - tokenURL: new URL("/oauth/token", url).toString(), - clientID: secrets.id, - clientSecret: secrets.secret, - scope: ["phone", "slack"], - passReqToCallback: true - }; - super(options, GroundTruthStrategy.passportCallback); - this.url = url; + if (config.admins.emails.includes(profile.email)) { + user.admin = true; } + await DB.from("users").where({ uuid: user.uuid }).update(user); + done(null, user); + } + + public readonly url: string; - public userProfile(accessToken: string, done: PassportProfileDone): void | PassportProfileDone { - (this._oauth2 as any)._request("GET", new URL("/api/user", this.url).toString(), null, - null, accessToken, (err: Error | null, data: string) => { - if (err) { - done(err); - return; - } - try { - const profile: IProfile = { - ...JSON.parse(data), - token: accessToken - }; - done(null, profile); - } catch (err) { - return done(err); - } - }); + constructor(url: string) { + const secrets = config.secrets.groundTruth; + if (!secrets || !secrets.id || !secrets.secret) { + throw new Error(`Client ID or secret not configured in config.json or environment variables for Ground + Truth`); } + const options: IOAuthStrategyOptions = { + authorizationURL: new URL("/oauth/authorize", url).toString(), + tokenURL: new URL("/oauth/token", url).toString(), + clientID: secrets.id, + clientSecret: secrets.secret, + scope: ["phone", "slack"], + passReqToCallback: true, + }; + super(options, GroundTruthStrategy.passportCallback); + this.url = url; + } + + public userProfile(accessToken: string, done: PassportProfileDone): void | PassportProfileDone { + (this._oauth2 as any)._request( + "GET", + new URL("/api/user", this.url).toString(), + null, + null, + accessToken, + (err: Error | null, data: string) => { + if (err) { + done(err); + return; + } + try { + const profile: IProfile = { + ...JSON.parse(data), + token: accessToken, + }; + done(null, profile); + } catch (err) { + return done(err); + } + } + ); + } } // Authentication helpers function getExternalPort(request: Request): number { - function defaultPort(): number { - // Default ports for HTTP and HTTPS - return request.protocol === "http" ? 80 : 443; - } - - const host = request.headers.host; - if (!host || Array.isArray(host)) { - return defaultPort(); - } - - // IPv6 literal support - const offset = host[0] === "[" ? host.indexOf("]") + 1 : 0; - const index = host.indexOf(":", offset); - if (index !== -1) { - return parseInt(host.substring(index + 1), 10); - } else { - return defaultPort(); - } + function defaultPort(): number { + // Default ports for HTTP and HTTPS + return request.protocol === "http" ? 80 : 443; + } + + const { host } = request.headers; + if (!host || Array.isArray(host)) { + return defaultPort(); + } + + // IPv6 literal support + const offset = host[0] === "[" ? host.indexOf("]") + 1 : 0; + const index = host.indexOf(":", offset); + if (index !== -1) { + return parseInt(host.substring(index + 1), 10); + } + return defaultPort(); } const validatedHostNames: string[] = []; -export function validateAndCacheHostName(request: Request, response: Response, next: NextFunction): void { - // Basically checks to see if the server behind the hostname has the same session key by HMACing a random nonce - if (validatedHostNames.find(hostname => hostname === request.hostname)) { - next(); - return; - } - - const nonce = crypto.randomBytes(64).toString("hex"); - function callback(message: http.IncomingMessage): void { - if (message.statusCode !== 200) { - console.error(`Got non-OK status code when validating hostname: ${request.hostname}`); - message.resume(); - return; - } - message.setEncoding("utf8"); - let data = ""; - message.on("data", (chunk) => data += chunk); - message.on("end", () => { - const localHMAC = crypto.createHmac("sha256", config.secrets.session).update(nonce).digest() - .toString("hex"); - if (localHMAC === data) { - validatedHostNames.push(request.hostname); - next(); - } else { - console.error(`Got invalid HMAC when validating hostname: ${request.hostname}`); - } - }); - } - function onError(err: Error): void { - console.error(`Error when validating hostname: ${request.hostname}`, err); - } - if (request.protocol === "http") { - http.get(`http://${request.hostname}:${getExternalPort(request)}/auth/validatehost/${nonce}`, callback) - .on("error", onError); - } else { - https.get(`https://${request.hostname}:${getExternalPort(request)}/auth/validatehost/${nonce}`, callback) - .on("error", onError); +export function validateAndCacheHostName( + request: Request, + response: Response, + next: NextFunction +): void { + // Basically checks to see if the server behind the hostname has the same session key by HMACing a random nonce + if (validatedHostNames.find(hostname => hostname === request.hostname)) { + next(); + return; + } + + const nonce = crypto.randomBytes(64).toString("hex"); + function callback(message: http.IncomingMessage): void { + if (message.statusCode !== 200) { + console.error(`Got non-OK status code when validating hostname: ${request.hostname}`); + message.resume(); + return; } + message.setEncoding("utf8"); + let data = ""; + message.on("data", chunk => (data += chunk)); + message.on("end", () => { + const localHMAC = crypto + .createHmac("sha256", config.secrets.session) + .update(nonce) + .digest() + .toString("hex"); + if (localHMAC === data) { + validatedHostNames.push(request.hostname); + next(); + } else { + console.error(`Got invalid HMAC when validating hostname: ${request.hostname}`); + } + }); + } + function onError(err: Error): void { + console.error(`Error when validating hostname: ${request.hostname}`, err); + } + if (request.protocol === "http") { + http + .get( + `http://${request.hostname}:${getExternalPort(request)}/auth/validatehost/${nonce}`, + callback + ) + .on("error", onError); + } else { + https + .get( + `https://${request.hostname}:${getExternalPort(request)}/auth/validatehost/${nonce}`, + callback + ) + .on("error", onError); + } } // export function createLink(request: Request, link: string): string { @@ -206,16 +235,19 @@ export function validateAndCacheHostName(request: Request, response: Response, n // } export function createLink(request: Request, link: string, proto?: string): string { - if (!proto) { - proto = "http"; - } - if (link[0] === "/") { - link = link.substring(1); - } - if ((request.secure && getExternalPort(request) === 443) - || (!request.secure && getExternalPort(request) === 80)) { - return `${proto}${request.secure ? "s" : ""}://${request.hostname}/${link}`; - } else { - return `${proto}${request.secure ? "s" : ""}://${request.hostname}:${getExternalPort(request)}/${link}`; - } + if (!proto) { + proto = "http"; + } + if (link[0] === "/") { + link = link.substring(1); + } + if ( + (request.secure && getExternalPort(request) === 443) || + (!request.secure && getExternalPort(request) === 80) + ) { + return `${proto}${request.secure ? "s" : ""}://${request.hostname}/${link}`; + } + return `${proto}${request.secure ? "s" : ""}://${request.hostname}:${getExternalPort( + request + )}/${link}`; } diff --git a/server/src/common.ts b/server/src/common.ts index cffa661..0091811 100644 --- a/server/src/common.ts +++ b/server/src/common.ts @@ -7,196 +7,204 @@ import "passport"; // Config // namespace IConfig { - export interface Secrets { - session: string; - groundTruth: { - url: string; - id: string; - secret: string; - }; - } - export interface Email { - from: string; - key: string; - } - export interface Server { - isProduction: boolean; - port: number; - cookieMaxAge: number; - cookieSecureOnly: boolean; - postgresURL: string; - defaultTimezone: string; - slackURL: string; - } + export interface Secrets { + session: string; + groundTruth: { + url: string; + id: string; + secret: string; + }; + } + export interface Email { + from: string; + key: string; + } + export interface Server { + isProduction: boolean; + port: number; + cookieMaxAge: number; + cookieSecureOnly: boolean; + postgresURL: string; + defaultTimezone: string; + slackURL: string; + } - export interface Main { - secrets: Secrets; - email: Email; - server: Server; - admins: { - domains: string[]; - emails: string[]; - }; - eventName: string; - } + export interface Main { + secrets: Secrets; + email: Email; + server: Server; + admins: { + domains: string[]; + emails: string[]; + }; + eventName: string; + } } class Config implements IConfig.Main { - public secrets: IConfig.Secrets = { - session: crypto.randomBytes(32).toString("hex"), - groundTruth: { - url: "https://login.hack.gt", - id: "", - secret: "" - } - }; - public email: IConfig.Email = { - from: "HackGT Team ", - key: "" - }; - public server: IConfig.Server = { - isProduction: false, - port: 3000, - cookieMaxAge: 1000 * 60 * 60 * 24 * 30 * 6, // 6 months - cookieSecureOnly: false, - postgresURL: "postgresql://localhost/bolt", - defaultTimezone: "America/New_York", - slackURL: "", - }; - public admins = { - domains: [] as string[], - emails: [] as string[] - }; - public eventName = "Untitled Event"; + public secrets: IConfig.Secrets = { + session: crypto.randomBytes(32).toString("hex"), + groundTruth: { + url: "https://login.hack.gt", + id: "", + secret: "", + }, + }; + + public email: IConfig.Email = { + from: "HackGT Team ", + key: "", + }; + + public server: IConfig.Server = { + isProduction: false, + port: 3000, + cookieMaxAge: 1000 * 60 * 60 * 24 * 30 * 6, // 6 months + cookieSecureOnly: false, + postgresURL: "postgresql://localhost/bolt", + defaultTimezone: "America/New_York", + slackURL: "", + }; - public sessionSecretSet = false; + public admins = { + domains: [] as string[], + emails: [] as string[], + }; + + public eventName = "Untitled Event"; + + public sessionSecretSet = false; + + constructor(fileName = "config.json") { + this.loadFromJSON(fileName); + this.loadFromEnv(); + if (!this.server.isProduction) { + this.eventName += " - Development"; + } + } + + protected loadFromJSON(fileName: string): void { + // tslint:disable-next-line:no-shadowed-variable + let config: Partial | null = null; + try { + config = JSON.parse(fs.readFileSync(path.resolve(__dirname, "./config", fileName), "utf8")); + } catch (err) { + if (err.code !== "ENOENT") { + throw err; + } + } + if (!config) { + return; + } + if (config.secrets) { + for (const key of Object.keys(config.secrets) as Array) { + // @ts-ignore + this.secrets[key] = config.secrets[key]; + } + } + if (config.email) { + for (const key of Object.keys(config.email) as Array) { + this.email[key] = config.email[key]; + } + } + if (config.server) { + for (const key of Object.keys(config.server) as Array) { + // @ts-ignore + this.server[key] = config.server[key]; + } + } + if (config.admins) { + if (config.admins.domains) { + this.admins.domains = config.admins.domains; + } + if (config.admins.emails) { + this.admins.emails = config.admins.emails; + } + } + if (config.eventName) { + this.eventName = config.eventName; + } + if (config.secrets && config.secrets.session) { + this.sessionSecretSet = true; + } + } - constructor(fileName: string = "config.json") { - this.loadFromJSON(fileName); - this.loadFromEnv(); - if (!this.server.isProduction) { - this.eventName += " - Development"; - } - } - protected loadFromJSON(fileName: string): void { - // tslint:disable-next-line:no-shadowed-variable - let config: Partial | null = null; - try { - config = JSON.parse(fs.readFileSync(path.resolve(__dirname, "./config", fileName), "utf8")); - } catch (err) { - if (err.code !== "ENOENT") { - throw err; - } - } - if (!config) { - return; - } - if (config.secrets) { - for (const key of Object.keys(config.secrets) as Array) { - // @ts-ignore - this.secrets[key] = config.secrets[key]; - } - } - if (config.email) { - for (const key of Object.keys(config.email) as Array) { - this.email[key] = config.email[key]; - } - } - if (config.server) { - for (const key of Object.keys(config.server) as Array) { - // @ts-ignore - this.server[key] = config.server[key]; - } - } - if (config.admins) { - if (config.admins.domains) { - this.admins.domains = config.admins.domains; - } - if (config.admins.emails) { - this.admins.emails = config.admins.emails; - } - } - if (config.eventName) { - this.eventName = config.eventName; - } - if (config.secrets && config.secrets.session) { - this.sessionSecretSet = true; - } - } - protected loadFromEnv(): void { - // Secrets - if (process.env.SESSION_SECRET) { - this.secrets.session = process.env.SESSION_SECRET; - this.sessionSecretSet = true; - } - if (process.env.GROUND_TRUTH_URL) { - this.secrets.groundTruth.url = process.env.GROUND_TRUTH_URL; - } - if (process.env.GROUND_TRUTH_ID) { - this.secrets.groundTruth.id = process.env.GROUND_TRUTH_ID; - } - if (process.env.GROUND_TRUTH_SECRET) { - this.secrets.groundTruth.secret = process.env.GROUND_TRUTH_SECRET; - } - // Email - if (process.env.EMAIL_FROM) { - this.email.from = process.env.EMAIL_FROM; - } - if (process.env.EMAIL_KEY) { - this.email.key = process.env.EMAIL_KEY; - } - // Server - if (process.env.PRODUCTION && process.env.PRODUCTION.toLowerCase() === "true") { - this.server.isProduction = true; - } - if (process.env.PORT) { - const port = parseInt(process.env.PORT!, 10); - if (!isNaN(port) && port > 0) { - this.server.port = port; - } - } - if (process.env.COOKIE_MAX_AGE) { - const maxAge = parseInt(process.env.COOKIE_MAX_AGE, 10); - if (!isNaN(maxAge) && maxAge > 0) { - this.server.cookieMaxAge = maxAge; - } - } - if (process.env.COOKIE_SECURE_ONLY && process.env.COOKIE_SECURE_ONLY.toLowerCase() === "true") { - this.server.cookieSecureOnly = true; - } - if (process.env.POSTGRES_URL) { - this.server.postgresURL = process.env.POSTGRES_URL; - } - if (process.env.DEFAULT_TIMEZONE) { - this.server.defaultTimezone = process.env.DEFAULT_TIMEZONE; - } - // Admins - if (process.env.ADMIN_EMAILS) { - this.admins.emails = JSON.parse(process.env.ADMIN_EMAILS!); - } - if (process.env.ADMIN_DOMAINS) { - this.admins.domains = JSON.parse(process.env.ADMIN_DOMAINS); - } - // Event name - if (process.env.EVENT_NAME) { - this.eventName = process.env.EVENT_NAME; - } + protected loadFromEnv(): void { + // Secrets + if (process.env.SESSION_SECRET) { + this.secrets.session = process.env.SESSION_SECRET; + this.sessionSecretSet = true; + } + if (process.env.GROUND_TRUTH_URL) { + this.secrets.groundTruth.url = process.env.GROUND_TRUTH_URL; + } + if (process.env.GROUND_TRUTH_ID) { + this.secrets.groundTruth.id = process.env.GROUND_TRUTH_ID; + } + if (process.env.GROUND_TRUTH_SECRET) { + this.secrets.groundTruth.secret = process.env.GROUND_TRUTH_SECRET; + } + // Email + if (process.env.EMAIL_FROM) { + this.email.from = process.env.EMAIL_FROM; + } + if (process.env.EMAIL_KEY) { + this.email.key = process.env.EMAIL_KEY; + } + // Server + if (process.env.PRODUCTION && process.env.PRODUCTION.toLowerCase() === "true") { + this.server.isProduction = true; + } + if (process.env.PORT) { + const port = parseInt(process.env.PORT!, 10); + if (!isNaN(port) && port > 0) { + this.server.port = port; + } + } + if (process.env.COOKIE_MAX_AGE) { + const maxAge = parseInt(process.env.COOKIE_MAX_AGE, 10); + if (!isNaN(maxAge) && maxAge > 0) { + this.server.cookieMaxAge = maxAge; + } + } + if (process.env.COOKIE_SECURE_ONLY && process.env.COOKIE_SECURE_ONLY.toLowerCase() === "true") { + this.server.cookieSecureOnly = true; + } + if (process.env.POSTGRES_URL) { + this.server.postgresURL = process.env.POSTGRES_URL; + } + if (process.env.DEFAULT_TIMEZONE) { + this.server.defaultTimezone = process.env.DEFAULT_TIMEZONE; + } + // Admins + if (process.env.ADMIN_EMAILS) { + this.admins.emails = JSON.parse(process.env.ADMIN_EMAILS!); + } + if (process.env.ADMIN_DOMAINS) { + this.admins.domains = JSON.parse(process.env.ADMIN_DOMAINS); + } + // Event name + if (process.env.EVENT_NAME) { + this.eventName = process.env.EVENT_NAME; + } - if (process.env.SLACK_WEBHOOK_URL) { - this.server.slackURL = process.env.SLACK_WEBHOOK_URL; - } + if (process.env.SLACK_WEBHOOK_URL) { + this.server.slackURL = process.env.SLACK_WEBHOOK_URL; } + } } -export let config = new Config(); +export const config = new Config(); // // Constants // export const PORT = config.server.port; -export const VERSION_NUMBER = JSON.parse(fs.readFileSync(path.resolve(__dirname, "./package.json"), "utf8")).version; +export const VERSION_NUMBER = JSON.parse( + fs.readFileSync(path.resolve(__dirname, "./package.json"), "utf8") +).version; export const COOKIE_OPTIONS = { - path: "/", - maxAge: config.server.cookieMaxAge, - secure: config.server.cookieSecureOnly, - httpOnly: true + path: "/", + maxAge: config.server.cookieMaxAge, + secure: config.server.cookieSecureOnly, + httpOnly: true, }; diff --git a/server/src/database.ts b/server/src/database.ts index eee75fb..cb351ce 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -1,20 +1,21 @@ -import {config} from "./common"; // // Database connection // import { Knex, knex } from "knex"; import * as path from "path"; +import { config } from "./common"; + const migrations = { - tableName: "knex_migrations", - directory: path.normalize(path.join(__dirname, "/migrations")) + tableName: "knex_migrations", + directory: path.normalize(path.join(__dirname, "/migrations")), }; const DBConfig: Knex.Config = { - client: "pg", - connection: config.server.postgresURL, - searchPath: ["knex", "public"], - migrations + client: "pg", + connection: config.server.postgresURL, + searchPath: ["knex", "public"], + migrations, }; export const DB = knex(DBConfig); @@ -22,9 +23,9 @@ export const DB = knex(DBConfig); // Set up sessions table used by connect-pg-simple (our session store) // From: https://github.com/voxpelli/node-connect-pg-simple/blob/master/table.sql async function setUpSessionsTable() { - const exists = await DB.schema.withSchema("public").hasTable("session"); - if (!exists) { - await DB.raw(` + const exists = await DB.schema.withSchema("public").hasTable("session"); + if (!exists) { + await DB.raw(` CREATE TABLE "session" ( "sid" varchar NOT NULL COLLATE "default", "sess" json NOT NULL, @@ -33,59 +34,61 @@ async function setUpSessionsTable() { WITH (OIDS=FALSE); ALTER TABLE "session" ADD CONSTRAINT "session_pkey" PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE; `); - console.log("Created session table"); - } + console.log("Created session table"); + } } -setUpSessionsTable().catch(err => { throw err; }); +setUpSessionsTable().catch(err => { + throw err; +}); export async function findUserByID(id: string): Promise { - const rows = await DB.from("users").where({ uuid: id }); - if (rows[0]) { - return rows[0] as IUser; - } - return null; + const rows = await DB.from("users").where({ uuid: id }); + if (rows[0]) { + return rows[0] as IUser; + } + return null; } export async function createRecord(tableName: string, data: T) { - await DB.into(tableName).insert(data); - return data; + await DB.into(tableName).insert(data); + return data; } // // Users // export interface IUser { - uuid: string; // Primary key - token: string; // Ground Truth token + uuid: string; // Primary key + token: string; // Ground Truth token - name: string; - email: string; - phone: string; - slackUsername: string; + name: string; + email: string; + phone: string; + slackUsername: string; - haveID: boolean; - admin: boolean; + haveID: boolean; + admin: boolean; } // // Items // export interface Category { - id: number; - name: string; + id: number; + name: string; } export interface IItem { - id: number; // Primary key - name: string; - description: string; - imageUrl: string; - categoryId: number; - totalAvailable: number; - maxRequestQty: number; - price: number; - hidden: boolean; - returnRequired: boolean; - approvalRequired: boolean; - owner: string; + id: number; // Primary key + name: string; + description: string; + imageUrl: string; + categoryId: number; + totalAvailable: number; + maxRequestQty: number; + price: number; + hidden: boolean; + returnRequired: boolean; + approvalRequired: boolean; + owner: string; } diff --git a/server/src/knexfile.ts b/server/src/knexfile.ts index 96a139d..9c0b67c 100644 --- a/server/src/knexfile.ts +++ b/server/src/knexfile.ts @@ -1,38 +1,39 @@ require("ts-node/register"); // this line is what makes Knex migrations work with TypeScript! Thanks to https://gist.github.com/tukkajukka/9893e5f111862d06044b73fa944a8741 const fs = require("fs"); - // Some duplicated code here from common.ts, but we can't use import in this file and we only need a couple values for knex const path = require("path"); let connectionUrl; if (!(process.env.PRODUCTION && process.env.PRODUCTION.toLowerCase() === "true")) { - let config; - try { - config = JSON.parse(fs.readFileSync(path.resolve(__dirname, "./config", "config.json"), "utf8")); - } catch (err) { - if (err.code !== "ENOENT") { - throw err; - } + let config; + try { + config = JSON.parse( + fs.readFileSync(path.resolve(__dirname, "./config", "config.json"), "utf8") + ); + } catch (err) { + if (err.code !== "ENOENT") { + throw err; } + } - if (config.server && config.server.postgresURL) { - connectionUrl = config.server.postgresURL; - } + if (config.server && config.server.postgresURL) { + connectionUrl = config.server.postgresURL; + } } if (process.env.POSTGRES_URL) { - // @ts-ignore - connectionUrl = process.env.POSTGRES_URL; + // @ts-ignore + connectionUrl = process.env.POSTGRES_URL; } const migrations = { - tableName: "knex_migrations", - directory: path.normalize(path.join(__dirname, "/migrations")) + tableName: "knex_migrations", + directory: path.normalize(path.join(__dirname, "/migrations")), }; module.exports = { - client: "pg", - connection: connectionUrl, - migrations + client: "pg", + connection: connectionUrl, + migrations, }; diff --git a/server/src/migrations/20190906232528_create_users_table.ts b/server/src/migrations/20190906232528_create_users_table.ts index 67fc47d..f8b4284 100644 --- a/server/src/migrations/20190906232528_create_users_table.ts +++ b/server/src/migrations/20190906232528_create_users_table.ts @@ -1,21 +1,19 @@ import { Knex } from "knex"; export async function up(knex: Knex): Promise { - return knex.schema.createTable("users", table => { - table.uuid("uuid").notNullable().unique().primary(); - table.string("token", 256); + return knex.schema.createTable("users", table => { + table.uuid("uuid").notNullable().unique().primary(); + table.string("token", 256); - table.text("name").notNullable(); - table.text("email").notNullable(); - table.text("phone").notNullable(); - table.text("slackUsername").notNullable(); - table.boolean("haveID").notNullable().defaultTo(false); - table.boolean("admin").notNullable().defaultTo(false); - }); + table.text("name").notNullable(); + table.text("email").notNullable(); + table.text("phone").notNullable(); + table.text("slackUsername").notNullable(); + table.boolean("haveID").notNullable().defaultTo(false); + table.boolean("admin").notNullable().defaultTo(false); + }); } - export async function down(knex: Knex): Promise { - return knex.schema.dropTableIfExists("users"); + return knex.schema.dropTableIfExists("users"); } - diff --git a/server/src/migrations/20190906235036_create_categories_table.ts b/server/src/migrations/20190906235036_create_categories_table.ts index 24116c8..8d3a0aa 100644 --- a/server/src/migrations/20190906235036_create_categories_table.ts +++ b/server/src/migrations/20190906235036_create_categories_table.ts @@ -1,14 +1,12 @@ import { Knex } from "knex"; export async function up(knex: Knex): Promise { - return knex.schema.createTable("categories", table => { - table.increments("category_id"); // use of increments also makes this the primary key - table.text("category_name").notNullable().unique(); - }); + return knex.schema.createTable("categories", table => { + table.increments("category_id"); // use of increments also makes this the primary key + table.text("category_name").notNullable().unique(); + }); } - export async function down(knex: Knex): Promise { - return knex.schema.dropTableIfExists("categories"); + return knex.schema.dropTableIfExists("categories"); } - diff --git a/server/src/migrations/20190906235544_create_items_table.ts b/server/src/migrations/20190906235544_create_items_table.ts index 3eb9b4d..ba10631 100644 --- a/server/src/migrations/20190906235544_create_items_table.ts +++ b/server/src/migrations/20190906235544_create_items_table.ts @@ -1,24 +1,27 @@ import { Knex } from "knex"; export async function up(knex: Knex): Promise { - return knex.schema.createTable("items", table => { - table.increments("item_id"); // use of increments also makes this the primary key - table.text("item_name").notNullable(); - table.text("description").notNullable(); - table.text("imageUrl").notNullable(); - table.integer("category_id").unsigned().references("category_id").inTable("categories").notNullable(); - table.integer("totalAvailable").notNullable(); - table.integer("maxRequestQty").notNullable(); - table.decimal("price", 6, 2).nullable().defaultTo(0); - table.boolean("hidden").notNullable().defaultTo(false); - table.boolean("returnRequired").notNullable().defaultTo(true); - table.boolean("approvalRequired").notNullable().defaultTo(true); - table.text("owner").notNullable(); - }); + return knex.schema.createTable("items", table => { + table.increments("item_id"); // use of increments also makes this the primary key + table.text("item_name").notNullable(); + table.text("description").notNullable(); + table.text("imageUrl").notNullable(); + table + .integer("category_id") + .unsigned() + .references("category_id") + .inTable("categories") + .notNullable(); + table.integer("totalAvailable").notNullable(); + table.integer("maxRequestQty").notNullable(); + table.decimal("price", 6, 2).nullable().defaultTo(0); + table.boolean("hidden").notNullable().defaultTo(false); + table.boolean("returnRequired").notNullable().defaultTo(true); + table.boolean("approvalRequired").notNullable().defaultTo(true); + table.text("owner").notNullable(); + }); } - export async function down(knex: Knex): Promise { - return knex.schema.dropTableIfExists("items"); + return knex.schema.dropTableIfExists("items"); } - diff --git a/server/src/migrations/20190906235755_create_requests_table.ts b/server/src/migrations/20190906235755_create_requests_table.ts index fb571e9..8affc14 100644 --- a/server/src/migrations/20190906235755_create_requests_table.ts +++ b/server/src/migrations/20190906235755_create_requests_table.ts @@ -1,33 +1,36 @@ import { Knex } from "knex"; export async function up(knex: Knex): Promise { - const REQUEST_STATUSES: string[] = [ - "SUBMITTED", - "APPROVED", - "DENIED", - "ABANDONED", - "CANCELLED", - "READY_FOR_PICKUP", - "FULFILLED", - "RETURNED", - "LOST", - "DAMAGED" - ]; + const REQUEST_STATUSES: string[] = [ + "SUBMITTED", + "APPROVED", + "DENIED", + "ABANDONED", + "CANCELLED", + "READY_FOR_PICKUP", + "FULFILLED", + "RETURNED", + "LOST", + "DAMAGED", + ]; - return knex.schema.createTable("requests", table => { - table.increments("request_id"); // use of increments also makes this the primary key - table.integer("request_item_id").unsigned().references("item_id").inTable("items").notNullable(); - table.integer("quantity").unsigned().notNullable(); - table.uuid("user_id").references("uuid").inTable("users").notNullable(); - table.enum("status", REQUEST_STATUSES); - table.timestamps(true, true); // adds timestamps with timezones to requests, - // the caveat is that as of 2019 the TIMESTAMP type will overflow in 2038... FYI future Earthlings working - // on this project. http://code.openark.org/blog/mysql/timestamp-vs-datetime-which-should-i-be-using - }); + return knex.schema.createTable("requests", table => { + table.increments("request_id"); // use of increments also makes this the primary key + table + .integer("request_item_id") + .unsigned() + .references("item_id") + .inTable("items") + .notNullable(); + table.integer("quantity").unsigned().notNullable(); + table.uuid("user_id").references("uuid").inTable("users").notNullable(); + table.enum("status", REQUEST_STATUSES); + table.timestamps(true, true); // adds timestamps with timezones to requests, + // the caveat is that as of 2019 the TIMESTAMP type will overflow in 2038... FYI future Earthlings working + // on this project. http://code.openark.org/blog/mysql/timestamp-vs-datetime-which-should-i-be-using + }); } - export async function down(knex: Knex): Promise { - return knex.schema.dropTableIfExists("requests"); + return knex.schema.dropTableIfExists("requests"); } - diff --git a/server/src/migrations/20191016191258_create_locations_table.ts b/server/src/migrations/20191016191258_create_locations_table.ts index 5fab885..b859a4b 100644 --- a/server/src/migrations/20191016191258_create_locations_table.ts +++ b/server/src/migrations/20191016191258_create_locations_table.ts @@ -1,14 +1,13 @@ import { Knex } from "knex"; export async function up(knex: Knex): Promise { - return knex.schema.createTable("locations", table => { - table.increments("location_id"); // use of increments also makes this the primary key - table.text("location_name").notNullable().unique(); - table.boolean("location_hidden").notNullable().defaultTo(false); - }); + return knex.schema.createTable("locations", table => { + table.increments("location_id"); // use of increments also makes this the primary key + table.text("location_name").notNullable().unique(); + table.boolean("location_hidden").notNullable().defaultTo(false); + }); } - export async function down(knex: Knex): Promise { - return knex.schema.dropTableIfExists("locations"); + return knex.schema.dropTableIfExists("locations"); } diff --git a/server/src/migrations/20191019160236_add_location_items.ts b/server/src/migrations/20191019160236_add_location_items.ts index d3bfe36..e59dcb4 100644 --- a/server/src/migrations/20191019160236_add_location_items.ts +++ b/server/src/migrations/20191019160236_add_location_items.ts @@ -1,27 +1,30 @@ import { Knex } from "knex"; export async function up(knex: Knex): Promise { - const existingLocations = await knex.from("locations"); - let defaultLocation; - if (!existingLocations.length) { // create a default location if one doesn't exist - defaultLocation = await knex("locations").insert({location_name: "HackGT Hardware Desk"}).returning(["location_id", "location_name"]); - defaultLocation = defaultLocation[0]; - } else { - defaultLocation = existingLocations[0]; - - } - return knex.schema.table("items", table => { - table.integer("location_id").unsigned() - .references("location_id").inTable("locations") - .notNullable().defaultTo(defaultLocation.location_id); - } - ); + const existingLocations = await knex.from("locations"); + let defaultLocation; + if (!existingLocations.length) { + // create a default location if one doesn't exist + defaultLocation = await knex("locations") + .insert({ location_name: "HackGT Hardware Desk" }) + .returning(["location_id", "location_name"]); + defaultLocation = defaultLocation[0]; + } else { + defaultLocation = existingLocations[0]; + } + return knex.schema.table("items", table => { + table + .integer("location_id") + .unsigned() + .references("location_id") + .inTable("locations") + .notNullable() + .defaultTo(defaultLocation.location_id); + }); } - export async function down(knex: Knex): Promise { - return knex.schema.table("locations", table => { - table.dropColumns("location_id", "location_name", "location_hidden"); - }); + return knex.schema.table("locations", table => { + table.dropColumns("location_id", "location_name", "location_hidden"); + }); } - diff --git a/server/src/migrations/20191023033514_create_settings_table.ts b/server/src/migrations/20191023033514_create_settings_table.ts index ca7ea65..316c726 100644 --- a/server/src/migrations/20191023033514_create_settings_table.ts +++ b/server/src/migrations/20191023033514_create_settings_table.ts @@ -1,13 +1,12 @@ import { Knex } from "knex"; export async function up(knex: Knex): Promise { - return knex.schema.createTable("settings", table => { - table.text("name").notNullable().unique().primary(); - table.text("value").notNullable(); - }); + return knex.schema.createTable("settings", table => { + table.text("name").notNullable().unique().primary(); + table.text("value").notNullable(); + }); } - export async function down(knex: Knex): Promise { - return knex.schema.dropTableIfExists("settings"); + return knex.schema.dropTableIfExists("settings"); } diff --git a/server/tsconfig.json b/server/tsconfig.json index dd4a027..d00ba44 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,30 +1,3 @@ { - "compilerOptions": { - "module": "commonjs", - "removeComments": true, - "preserveConstEnums": true, - "outDir": "./build", - "sourceMap": true, - "target": "es6", - "lib": [ - "es2017", - "esnext.asynciterable" - ], - "strict": true, - "noImplicitAny": false, - "strictNullChecks": true, - "noImplicitThis": true, - "alwaysStrict": true, - "noUnusedLocals": true, - "noUnusedParameters": false, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules" - ] + "extends": "@hex-labs/tsconfig" } From bd936aa7ae864c8333291f093735096d9785a565 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Mon, 12 Apr 2021 23:53:43 -0400 Subject: [PATCH 02/17] Add basic client linting --- client/.eslintrc.json | 6 + client/package-lock.json | 2945 +++++++++++++---- client/package.json | 16 +- client/src/App.test.tsx | 11 +- client/src/App.tsx | 192 +- client/src/components/FeedbackLink.tsx | 48 +- client/src/components/Footer.tsx | 32 +- client/src/components/HomeContainer.tsx | 117 +- client/src/components/Navigation.tsx | 102 +- .../src/components/admin/AdminLinksCard.tsx | 69 +- .../admin/AdminOverviewContainer.tsx | 118 +- .../components/admin/AdminRequestsWrapper.tsx | 157 +- .../components/admin/AdminUsersListTable.tsx | 370 ++- .../admin/AdminUsersListWrapper.tsx | 78 +- client/src/components/csv/CSVReview.tsx | 97 +- client/src/components/csv/CSVUpload.tsx | 358 +- client/src/components/csv/CSVWizard.tsx | 363 +- client/src/components/desk/CardList.tsx | 93 +- client/src/components/desk/DeskContainer.tsx | 342 +- client/src/components/desk/DeskUtil.tsx | 43 +- .../src/components/desk/ItemAndQuantity.tsx | 24 +- .../desk/fulfillment/ReadyToPrepareCard.tsx | 205 +- .../desk/fulfillment/ReadyToPrepareList.tsx | 42 +- .../components/desk/pickup/PhotoIdCheck.tsx | 170 +- .../desk/pickup/ReadyForPickupCard.tsx | 228 +- .../desk/pickup/ReadyForPickupList.tsx | 41 +- .../components/desk/returns/PhotoIdReturn.tsx | 273 +- .../desk/returns/ReadyForReturnCard.tsx | 285 +- .../desk/returns/ReadyForReturnList.tsx | 45 +- .../desk/submitted/SubmittedCard.tsx | 181 +- .../desk/submitted/SubmittedList.tsx | 65 +- .../components/inventory/HardwareCategory.tsx | 41 +- .../src/components/inventory/HardwareItem.tsx | 296 +- .../components/inventory/HardwareLocation.tsx | 22 +- .../inventory/HardwareLocationContents.tsx | 149 +- .../components/inventory/NewHardwareList.tsx | 225 +- .../src/components/inventory/NoItemsFound.tsx | 71 +- .../components/inventory/RequestButton.tsx | 165 +- .../src/components/item/CreateItemWrapper.tsx | 55 +- .../src/components/item/EditItemWrapper.tsx | 81 +- client/src/components/item/ItemEditForm.tsx | 918 ++--- client/src/components/item/ItemWrapper.tsx | 29 +- .../reports/demand/ItemDemandReport.tsx | 391 ++- .../statistics/DetailedItemStatistics.tsx | 248 +- .../src/components/requests/RequestedList.tsx | 291 +- .../users/EditUserProfileWrapper.tsx | 121 +- client/src/components/users/UserProfile.tsx | 470 +-- .../components/users/UserProfileWrapper.tsx | 29 +- .../src/components/util/AddOptionDropdown.tsx | 130 +- client/src/components/util/CacheBuster.tsx | 106 +- client/src/components/util/LoadingSpinner.tsx | 22 +- client/src/components/util/PrivateRoute.tsx | 48 +- .../src/components/util/graphql/Mutations.ts | 206 +- client/src/components/util/graphql/Queries.ts | 389 +-- .../components/util/graphql/Subscriptions.ts | 54 +- client/src/index.tsx | 146 +- client/src/serviceWorker.ts | 36 +- client/src/state/Account.ts | 17 +- client/src/state/Desk.ts | 62 +- client/src/state/Store.ts | 24 +- client/src/types/Hardware.ts | 106 +- client/src/types/Request.ts | 44 +- client/src/types/Setting.ts | 4 +- client/src/types/User.ts | 20 +- 64 files changed, 7159 insertions(+), 4973 deletions(-) create mode 100644 client/.eslintrc.json diff --git a/client/.eslintrc.json b/client/.eslintrc.json new file mode 100644 index 0000000..f048072 --- /dev/null +++ b/client/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@hex-labs/eslint-config-react", + "rules": { + "camelcase": "off" + } +} diff --git a/client/package-lock.json b/client/package-lock.json index d7a4145..77b91cc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -108,14 +108,6 @@ } } }, - "@apollo/react-ssr": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@apollo/react-ssr/-/react-ssr-4.0.0.tgz", - "integrity": "sha512-IW+x5M6eTTMbKwCfgox+BwfnZVhKTNiAjAHVdeOIc5jHIuG4oyB/gWcEonHRK6RKb+zLV1rOqpdwnJc6wchxLA==", - "requires": { - "@apollo/client": "^3.3.14" - } - }, "@ardatan/aggregate-error": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@ardatan/aggregate-error/-/aggregate-error-0.0.6.tgz", @@ -2341,6 +2333,41 @@ "@hapi/hoek": "^8.3.0" } }, + "@hex-labs/eslint-config-react": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@hex-labs/eslint-config-react/-/eslint-config-react-1.1.1.tgz", + "integrity": "sha512-yxwjfHAGieC+Eku7t0FsTGh/LaKkU8tqloRAtZpbkoDVAmqf7G92LtWNgJTqqKLzSObWF61+j5axJk7TOl3SPA==", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "^4.9.0", + "@typescript-eslint/parser": "^4.13.0", + "eslint-config-airbnb": "^18.2.1", + "eslint-config-prettier": "^7.1.0", + "eslint-config-react-app": "^6.0.0", + "eslint-plugin-flowtype": "^5.2.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-react": "^7.21.5", + "eslint-plugin-react-hooks": "^4.2.0" + } + }, + "@hex-labs/prettier-config": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@hex-labs/prettier-config/-/prettier-config-1.1.1.tgz", + "integrity": "sha512-GuF+VTq1THJa4tAgJUphMXT2Knp9rGOb3VIBMLQtEgiBS03S5HDS8tmRgcfoIxTKAOQWXYX8sxlFlHnVYgTtLg==", + "dev": true + }, + "@hex-labs/stylelint-config": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@hex-labs/stylelint-config/-/stylelint-config-1.1.1.tgz", + "integrity": "sha512-zeeD7ebR+difr9510xSm4gm4D30ygJlrovuwKTg1uXOwN3FYJULPQR+qrmPxX2uxU46YRZsu+yWpFGHAyC7FXw==", + "dev": true, + "requires": { + "stylelint-config-prettier": "^8.0.2", + "stylelint-config-rational-order": "^0.1.2", + "stylelint-config-standard": "^20.0.0" + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2889,6 +2916,16 @@ } } }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", @@ -3016,6 +3053,109 @@ "@sinonjs/commons": "^1.7.0" } }, + "@stylelint/postcss-css-in-js": { + "version": "0.37.2", + "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz", + "integrity": "sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA==", + "dev": true, + "requires": { + "@babel/core": ">=7.9.0" + } + }, + "@stylelint/postcss-markdown": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@stylelint/postcss-markdown/-/postcss-markdown-0.36.2.tgz", + "integrity": "sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ==", + "dev": true, + "requires": { + "remark": "^13.0.0", + "unist-util-find-all-after": "^3.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "remark": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/remark/-/remark-13.0.0.tgz", + "integrity": "sha512-HDz1+IKGtOyWN+QgBiAT0kn+2s6ovOxHyPAFGKVE81VSzJ+mq7RwHFledEvB5F1p4iJvOah/LOKdFuzvRnNLCA==", + "dev": true, + "requires": { + "remark-parse": "^9.0.0", + "remark-stringify": "^9.0.0", + "unified": "^9.1.0" + } + }, + "remark-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "dev": true, + "requires": { + "mdast-util-from-markdown": "^0.8.0" + } + }, + "remark-stringify": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-9.0.1.tgz", + "integrity": "sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg==", + "dev": true, + "requires": { + "mdast-util-to-markdown": "^0.6.0" + } + }, + "unified": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.1.tgz", + "integrity": "sha512-juWjuI8Z4xFg8pJbnEZ41b5xjGUWGHqXALmBZ3FC3WX0PIx1CZBIIJ6mXbYMcf6Yw4Fi0rFUTA1cdz/BglbOhA==", + "dev": true, + "requires": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + } + }, + "unist-util-find-all-after": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-3.0.2.tgz", + "integrity": "sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ==", + "dev": true, + "requires": { + "unist-util-is": "^4.0.0" + } + }, + "unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "dev": true + }, + "vfile": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + } + } + } + }, "@surma/rollup-plugin-off-main-thread": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-1.4.2.tgz", @@ -3299,11 +3439,26 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "@types/mdast": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz", + "integrity": "sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw==", + "dev": true, + "requires": { + "@types/unist": "*" + } + }, "@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==" }, + "@types/minimist": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz", + "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", + "dev": true + }, "@types/node": { "version": "14.14.37", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz", @@ -3664,6 +3819,32 @@ } } }, + "@types/unist": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", + "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==", + "dev": true + }, + "@types/vfile": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-3.0.2.tgz", + "integrity": "sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/unist": "*", + "@types/vfile-message": "*" + } + }, + "@types/vfile-message": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/vfile-message/-/vfile-message-2.0.0.tgz", + "integrity": "sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw==", + "dev": true, + "requires": { + "vfile-message": "*" + } + }, "@types/webpack": { "version": "4.41.27", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.27.tgz", @@ -4010,11 +4191,6 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -4109,11 +4285,6 @@ "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" - }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -4198,15 +4369,6 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -4262,7 +4424,8 @@ "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true }, "array-flatten": { "version": "2.1.2", @@ -4393,9 +4556,10 @@ "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=" }, "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true }, "async": { "version": "2.6.3", @@ -4410,11 +4574,6 @@ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" }, - "async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" - }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -5061,6 +5220,12 @@ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" }, + "bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "dev": true + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5508,6 +5673,12 @@ "get-intrinsic": "^1.0.2" } }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "dev": true + }, "caller-callsite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", @@ -5557,22 +5728,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" - } - } - }, "camelize": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", @@ -5612,6 +5767,12 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, + "ccount": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", + "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", + "dev": true + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5627,6 +5788,30 @@ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" }, + "character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true + }, + "character-entities-html4": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", + "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", + "dev": true + }, + "character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true + }, + "character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true + }, "check-types": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", @@ -5751,6 +5936,16 @@ "wrap-ansi": "^6.2.0" } }, + "clone-regexp": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.1.tgz", + "integrity": "sha512-Fcij9IwRW27XedRIJnSOEupS7RVcXtObJXbcUOX93UCLqqOdRpkvzKywOOSizmEK/Is3S/RHX9dLdfo6R1Q1mw==", + "dev": true, + "requires": { + "is-regexp": "^1.0.0", + "is-supported-regexp-flag": "^1.0.0" + } + }, "clsx": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", @@ -5771,10 +5966,11 @@ "q": "^1.1.2" } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "collapse-white-space": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", + "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", + "dev": true }, "collect-v8-coverage": { "version": "1.0.1", @@ -5930,11 +6126,6 @@ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -6477,6 +6668,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, "requires": { "array-find-index": "^1.0.1" } @@ -6536,6 +6728,16 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + } + }, "decimal.js": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", @@ -6699,11 +6901,6 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -7077,11 +7274,6 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" }, - "env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" - }, "errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", @@ -7303,6 +7495,11 @@ "color-convert": "^2.0.1" } }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==" + }, "chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -7325,6 +7522,28 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "requires": { + "flat-cache": "^3.0.4" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", + "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==" + }, "globals": { "version": "13.8.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.8.0.tgz", @@ -7343,6 +7562,11 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -7356,6 +7580,16 @@ "lru-cache": "^6.0.0" } }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7364,6 +7598,35 @@ "has-flag": "^4.0.0" } }, + "table": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.9.tgz", + "integrity": "sha512-F3cLs9a3hL1Z7N4+EkSscsel3z55XT950AvB05bwayrNg5T1/gykXtigioTAjbltvbMSJvvhFCbnf6mX+ntnJQ==", + "requires": { + "ajv": "^8.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "lodash.clonedeep": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" + }, + "dependencies": { + "ajv": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.1.0.tgz", + "integrity": "sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + } + } + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -7371,6 +7634,34 @@ } } }, + "eslint-config-airbnb": { + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz", + "integrity": "sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg==", + "dev": true, + "requires": { + "eslint-config-airbnb-base": "^14.2.1", + "object.assign": "^4.1.2", + "object.entries": "^1.1.2" + } + }, + "eslint-config-airbnb-base": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", + "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.2" + } + }, + "eslint-config-prettier": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz", + "integrity": "sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==", + "dev": true + }, "eslint-config-react-app": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz", @@ -7985,6 +8276,15 @@ } } }, + "execall": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execall/-/execall-1.0.0.tgz", + "integrity": "sha1-c9CQTjlbPKsGWLCNCewlMH8pu3M=", + "dev": true, + "requires": { + "clone-regexp": "^1.0.0" + } + }, "exenv": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", @@ -8284,6 +8584,12 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "dev": true + }, "fastq": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", @@ -8333,11 +8639,12 @@ "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==" }, "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-4.0.0.tgz", + "integrity": "sha512-AVSwsnbV8vH/UVbvgEhf3saVQXORNv0ZzSkvkhQIaia5Tia+JhGTaa/ePUSVoPHQyGayQNmYfkzFi3WZV5zcpA==", + "dev": true, "requires": { - "flat-cache": "^3.0.4" + "flat-cache": "^2.0.1" } }, "file-loader": { @@ -8434,18 +8741,32 @@ } }, "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "flatted": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", - "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true }, "flatten": { "version": "1.0.3", @@ -8690,62 +9011,6 @@ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "requires": { - "globule": "^1.0.0" - } - }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8776,11 +9041,6 @@ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" - }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -8823,6 +9083,12 @@ "is-glob": "^4.0.1" } }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", + "dev": true + }, "global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -8869,15 +9135,20 @@ "slash": "^3.0.0" } }, - "globule": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.2.tgz", - "integrity": "sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==", - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.10", - "minimatch": "~3.0.2" - } + "globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=", + "dev": true + }, + "gonzales-pe": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } }, "graceful-fs": { "version": "4.2.6", @@ -8991,6 +9262,12 @@ "har-schema": "^2.0.0" } }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true + }, "harmony-reflect": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.1.tgz", @@ -9004,21 +9281,6 @@ "function-bind": "^1.1.1" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - } - } - }, "has-bigints": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", @@ -9034,11 +9296,6 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -9233,6 +9490,12 @@ "terser": "^4.6.3" } }, + "html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=", + "dev": true + }, "html-webpack-plugin": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz", @@ -9569,6 +9832,12 @@ "resolve-from": "^5.0.0" } }, + "import-lazy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", + "integrity": "sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==", + "dev": true + }, "import-local": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", @@ -9593,14 +9862,6 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "requires": { - "repeating": "^2.0.0" - } - }, "indexes-of": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", @@ -9649,14 +9910,6 @@ "side-channel": "^1.0.4" } }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "requires": { - "loose-envify": "^1.0.0" - } - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -9695,6 +9948,28 @@ } } }, + "is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true + }, + "is-alphanumeric": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", + "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=", + "dev": true + }, + "is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, + "requires": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + } + }, "is-arguments": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", @@ -9792,6 +10067,12 @@ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" }, + "is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true + }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", @@ -9829,11 +10110,6 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, - "is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==" - }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -9852,6 +10128,12 @@ "is-extglob": "^2.1.1" } }, + "is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true + }, "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -9962,6 +10244,12 @@ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==" }, + "is-supported-regexp-flag": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.1.tgz", + "integrity": "sha512-3vcJecUUrpgCqc/ca0aWeNu64UGgxcvO60K/Fkr1N6RSvfGCTU60UKN68JDmKokgba0rFFJs12EnzOQa14ubKQ==", + "dev": true + }, "is-symbol": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", @@ -9975,16 +10263,29 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-whitespace-character": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", + "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", + "dev": true }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" }, + "is-word-character": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", + "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", + "dev": true + }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -11594,11 +11895,6 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz", "integrity": "sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ==" }, - "js-base64": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", - "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11775,6 +12071,12 @@ "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==" }, + "known-css-properties": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.11.0.tgz", + "integrity": "sha512-bEZlJzXo5V/ApNNa5z375mJC6Nrz4vG43UgcSCrg2OHC+yuB6j0iDSrY7RQ/+PRofFB03wNIIt9iXIVLr4wc7w==", + "dev": true + }, "language-subtag-registry": { "version": "0.3.21", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", @@ -11816,18 +12118,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, "loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", @@ -11918,11 +12208,26 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, "loglevel": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==" }, + "longest-streak": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", + "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -11935,6 +12240,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, "requires": { "currently-unhandled": "^0.4.1", "signal-exit": "^3.0.0" @@ -12008,7 +12314,8 @@ "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true }, "map-visit": { "version": "1.0.0", @@ -12018,6 +12325,24 @@ "object-visit": "^1.0.0" } }, + "markdown-escapes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", + "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", + "dev": true + }, + "markdown-table": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", + "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==", + "dev": true + }, + "mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -12028,6 +12353,80 @@ "safe-buffer": "^5.1.2" } }, + "mdast-util-compact": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz", + "integrity": "sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==", + "dev": true, + "requires": { + "unist-util-visit": "^1.1.0" + } + }, + "mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "dependencies": { + "parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "requires": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + } + } + }, + "mdast-util-to-markdown": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz", + "integrity": "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "longest-streak": "^2.0.0", + "mdast-util-to-string": "^2.0.0", + "parse-entities": "^2.0.0", + "repeat-string": "^1.0.0", + "zwitch": "^1.0.0" + }, + "dependencies": { + "parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "requires": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + } + } + }, + "mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "dev": true + }, "mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -12047,23 +12446,6 @@ "readable-stream": "^2.0.1" } }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - } - }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -12089,6 +12471,32 @@ "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==" }, + "micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + }, + "dependencies": { + "parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "requires": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + } + } + }, "micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", @@ -12137,6 +12545,12 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, "mini-create-react-context": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz", @@ -12225,6 +12639,24 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + }, + "dependencies": { + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + } + } + }, "minipass": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", @@ -12355,7 +12787,8 @@ "nan": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "optional": true }, "nanoid": { "version": "2.1.11", @@ -12439,33 +12872,6 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" }, - "node-gyp": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz", - "integrity": "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==", - "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.3", - "nopt": "^5.0.0", - "npmlog": "^4.1.2", - "request": "^2.88.2", - "rimraf": "^3.0.2", - "semver": "^7.3.2", - "tar": "^6.0.2", - "which": "^2.0.2" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -12559,74 +12965,6 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==" }, - "node-sass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-5.0.0.tgz", - "integrity": "sha512-opNgmlu83ZCF792U281Ry7tak9IbVC+AKnXGovcQ8LG8wFaJv6cLnRlc6DIHlmNxWEexB5bZxi9SZ9JyUuOYjw==", - "requires": { - "async-foreach": "^0.1.3", - "chalk": "^1.1.1", - "cross-spawn": "^7.0.3", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "lodash": "^4.17.15", - "meow": "^3.7.0", - "mkdirp": "^0.5.1", - "nan": "^2.13.2", - "node-gyp": "^7.1.0", - "npmlog": "^4.0.0", - "request": "^2.88.0", - "sass-graph": "2.2.5", - "stdout-stream": "^1.4.0", - "true-case-path": "^1.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "requires": { - "abbrev": "1" - } - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -12658,6 +12996,12 @@ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" }, + "normalize-selector": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz", + "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=", + "dev": true + }, "normalize-url": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", @@ -12684,17 +13028,6 @@ } } }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -12713,11 +13046,6 @@ "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -13070,6 +13398,20 @@ "safe-buffer": "^5.1.1" } }, + "parse-entities": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", + "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", + "dev": true, + "requires": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", @@ -13634,6 +13976,15 @@ "postcss": "^7.0.2" } }, + "postcss-html": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.36.0.tgz", + "integrity": "sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==", + "dev": true, + "requires": { + "htmlparser2": "^3.10.0" + } + }, "postcss-image-set-function": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz", @@ -13652,6 +14003,15 @@ "postcss": "^7.0.2" } }, + "postcss-jsx": { + "version": "0.36.4", + "resolved": "https://registry.npmjs.org/postcss-jsx/-/postcss-jsx-0.36.4.tgz", + "integrity": "sha512-jwO/7qWUvYuWYnpOb0+4bIIgJt7003pgU3P6nETBLaOyBXuTD55ho21xnals5nBrlpTIFodyd3/jBi6UO3dHvA==", + "dev": true, + "requires": { + "@babel/core": ">=7.2.2" + } + }, "postcss-lab-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz", @@ -13662,6 +14022,15 @@ "postcss-values-parser": "^2.0.0" } }, + "postcss-less": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.4.tgz", + "integrity": "sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, "postcss-load-config": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.2.tgz", @@ -13756,6 +14125,16 @@ "postcss": "^7.0.2" } }, + "postcss-markdown": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/postcss-markdown/-/postcss-markdown-0.36.0.tgz", + "integrity": "sha512-rl7fs1r/LNSB2bWRhyZ+lM/0bwKv9fhl38/06gF6mKMo/NPnp55+K1dSTosSVjFZc0e1ppBlu+WT91ba0PMBfQ==", + "dev": true, + "requires": { + "remark": "^10.0.1", + "unist-util-find-all-after": "^1.0.2" + } + }, "postcss-media-minmax": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz", @@ -13764,6 +14143,12 @@ "postcss": "^7.0.2" } }, + "postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", + "dev": true + }, "postcss-merge-longhand": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", @@ -14241,6 +14626,24 @@ "postcss": "^7.0.2" } }, + "postcss-reporter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-6.0.1.tgz", + "integrity": "sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "lodash": "^4.17.11", + "log-symbols": "^2.2.0", + "postcss": "^7.0.7" + } + }, + "postcss-resolve-nested-selector": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", + "integrity": "sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=", + "dev": true + }, "postcss-safe-parser": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-5.0.2.tgz", @@ -14271,12 +14674,31 @@ } } }, - "postcss-selector-matches": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz", - "integrity": "sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==", + "postcss-sass": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.3.5.tgz", + "integrity": "sha512-B5z2Kob4xBxFjcufFnhQ2HqJQ2y/Zs/ic5EZbCywCkxKd756Q40cIQ/veRDwSrw1BF6+4wUgmpm0sBASqVi65A==", + "dev": true, "requires": { - "balanced-match": "^1.0.0", + "gonzales-pe": "^4.2.3", + "postcss": "^7.0.1" + } + }, + "postcss-scss": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.1.1.tgz", + "integrity": "sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==", + "dev": true, + "requires": { + "postcss": "^7.0.6" + } + }, + "postcss-selector-matches": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz", + "integrity": "sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==", + "requires": { + "balanced-match": "^1.0.0", "postcss": "^7.0.2" } }, @@ -14300,6 +14722,16 @@ "util-deprecate": "^1.0.2" } }, + "postcss-sorting": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-4.1.0.tgz", + "integrity": "sha512-r4T2oQd1giURJdHQ/RMb72dKZCuLOdWx2B/XhXN1Y1ZdnwXsKH896Qz6vD4tFy9xSjpKNYhlZoJmWyhH/7JUQw==", + "dev": true, + "requires": { + "lodash": "^4.17.4", + "postcss": "^7.0.0" + } + }, "postcss-svgo": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz", @@ -14317,6 +14749,12 @@ } } }, + "postcss-syntax": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", + "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", + "dev": true + }, "postcss-unique-selectors": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", @@ -14352,6 +14790,12 @@ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" }, + "prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true + }, "pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -14572,6 +15016,12 @@ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, "raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -15054,56 +15504,6 @@ } } }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "dependencies": { - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - } - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "requires": { - "pinkie-promise": "^2.0.0" - } - } - } - }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -15135,15 +15535,6 @@ "minimatch": "3.0.4" } }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, "redux": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", @@ -15164,11 +15555,6 @@ "redux-devtools-instrument": "^1.9.0" } }, - "redux-devtools-extension": { - "version": "2.13.9", - "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz", - "integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==" - }, "redux-devtools-instrument": { "version": "1.9.6", "resolved": "https://registry.npmjs.org/redux-devtools-instrument/-/redux-devtools-instrument-1.9.6.tgz", @@ -15179,35 +15565,6 @@ "symbol-observable": "^1.0.2" } }, - "redux-immutable-state-invariant": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz", - "integrity": "sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg==", - "requires": { - "invariant": "^2.1.0", - "json-stringify-safe": "^5.0.1" - } - }, - "redux-starter-kit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redux-starter-kit/-/redux-starter-kit-2.0.0.tgz", - "integrity": "sha512-s/63wbRB1Az4pnTD36JtBld7C/Wrxu73Lckf3Je6RKHFk9GoIkJG5OfEZdMm6WukSPYoloKtkUjrWpF1gc/VgA==", - "requires": { - "immer": "^4.0.1", - "redux": "^4.0.0", - "redux-devtools-extension": "^2.13.8", - "redux-immutable-state-invariant": "^2.1.0", - "redux-thunk": "^2.3.0", - "reselect": "^4.0.0" - }, - "dependencies": { - "immer": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/immer/-/immer-4.0.2.tgz", - "integrity": "sha512-Q/tm+yKqnKy4RIBmmtISBlhXuSDrB69e9EKTYiIenIKQkXBQir43w+kN/eGiax3wt1J0O1b2fYcNqLSbEcXA7w==" - } - } - }, "redux-thunk": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", @@ -15397,6 +15754,62 @@ "fbjs": "^3.0.0" } }, + "remark": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-10.0.1.tgz", + "integrity": "sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ==", + "dev": true, + "requires": { + "remark-parse": "^6.0.0", + "remark-stringify": "^6.0.0", + "unified": "^7.0.0" + } + }, + "remark-parse": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-6.0.3.tgz", + "integrity": "sha512-QbDXWN4HfKTUC0hHa4teU463KclLAnwpn/FBn87j9cKYJWWawbiLgMfP2Q4XwhxxuuuOxHlw+pSN0OKuJwyVvg==", + "dev": true, + "requires": { + "collapse-white-space": "^1.0.2", + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "is-word-character": "^1.0.0", + "markdown-escapes": "^1.0.0", + "parse-entities": "^1.1.0", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "trim": "0.0.1", + "trim-trailing-lines": "^1.0.0", + "unherit": "^1.0.4", + "unist-util-remove-position": "^1.0.0", + "vfile-location": "^2.0.0", + "xtend": "^4.0.1" + } + }, + "remark-stringify": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz", + "integrity": "sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==", + "dev": true, + "requires": { + "ccount": "^1.0.0", + "is-alphanumeric": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "longest-streak": "^2.0.1", + "markdown-escapes": "^1.0.0", + "markdown-table": "^1.1.0", + "mdast-util-compact": "^1.0.0", + "parse-entities": "^1.0.2", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "stringify-entities": "^1.0.1", + "unherit": "^1.0.4", + "xtend": "^4.0.1" + } + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -15444,13 +15857,11 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "requires": { - "is-finite": "^1.0.0" - } + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true }, "request": { "version": "2.88.2", @@ -15529,11 +15940,6 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, - "reselect": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", - "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" - }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -15938,136 +16344,6 @@ "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-10.0.0.tgz", "integrity": "sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg==" }, - "sass-graph": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", - "integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==", - "requires": { - "glob": "^7.0.0", - "lodash": "^4.0.0", - "scss-tokenizer": "^0.2.3", - "yargs": "^13.3.2" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, "sass-loader": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.1.1.tgz", @@ -16132,25 +16408,6 @@ "ajv-keywords": "^3.5.2" } }, - "scss-tokenizer": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", - "requires": { - "js-base64": "^2.1.8", - "source-map": "^0.4.2" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -16468,37 +16725,23 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - } - } + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } }, "snapdragon": { "version": "0.8.2", @@ -16769,6 +17012,12 @@ } } }, + "specificity": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", + "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", + "dev": true + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -16844,6 +17093,12 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz", "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==" }, + "state-toggle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", + "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", + "dev": true + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -16868,14 +17123,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, - "stdout-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "requires": { - "readable-stream": "^2.0.1" - } - }, "stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", @@ -16990,6 +17237,18 @@ "safe-buffer": "~5.1.0" } }, + "stringify-entities": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", + "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", + "dev": true, + "requires": { + "character-entities-html4": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, "stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", @@ -17015,14 +17274,6 @@ "ansi-regex": "^5.0.0" } }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "^0.2.0" - } - }, "strip-comments": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-1.0.2.tgz", @@ -17042,14 +17293,6 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "requires": { - "get-stdin": "^4.0.1" - } - }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -17069,6 +17312,12 @@ "schema-utils": "^2.7.0" } }, + "style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=", + "dev": true + }, "styled-components": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.3.tgz", @@ -17108,6 +17357,1096 @@ } } }, + "stylelint": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.12.0.tgz", + "integrity": "sha512-P8O1xDy41B7O7iXaSlW+UuFbE5+ZWQDb61ndGDxKIt36fMH50DtlQTbwLpFLf8DikceTAb3r6nPrRv30wBlzXw==", + "dev": true, + "requires": { + "@stylelint/postcss-css-in-js": "^0.37.2", + "@stylelint/postcss-markdown": "^0.36.2", + "autoprefixer": "^9.8.6", + "balanced-match": "^1.0.0", + "chalk": "^4.1.0", + "cosmiconfig": "^7.0.0", + "debug": "^4.3.1", + "execall": "^2.0.0", + "fast-glob": "^3.2.5", + "fastest-levenshtein": "^1.0.12", + "file-entry-cache": "^6.0.1", + "get-stdin": "^8.0.0", + "global-modules": "^2.0.0", + "globby": "^11.0.2", + "globjoin": "^0.1.4", + "html-tags": "^3.1.0", + "ignore": "^5.1.8", + "import-lazy": "^4.0.0", + "imurmurhash": "^0.1.4", + "known-css-properties": "^0.21.0", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "mathml-tag-names": "^2.1.3", + "meow": "^9.0.0", + "micromatch": "^4.0.2", + "normalize-selector": "^0.2.0", + "postcss": "^7.0.35", + "postcss-html": "^0.36.0", + "postcss-less": "^3.1.4", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-safe-parser": "^4.0.2", + "postcss-sass": "^0.4.4", + "postcss-scss": "^2.1.1", + "postcss-selector-parser": "^6.0.4", + "postcss-syntax": "^0.36.2", + "postcss-value-parser": "^4.1.0", + "resolve-from": "^5.0.0", + "slash": "^3.0.0", + "specificity": "^0.4.1", + "string-width": "^4.2.2", + "strip-ansi": "^6.0.0", + "style-search": "^0.1.0", + "sugarss": "^2.0.0", + "svg-tags": "^1.0.0", + "table": "^6.0.7", + "v8-compile-cache": "^2.2.0", + "write-file-atomic": "^3.0.3" + }, + "dependencies": { + "ajv": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.1.0.tgz", + "integrity": "sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "clone-regexp": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", + "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==", + "dev": true, + "requires": { + "is-regexp": "^2.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "execall": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz", + "integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==", + "dev": true, + "requires": { + "clone-regexp": "^2.1.0" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", + "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", + "dev": true + }, + "get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "hosted-git-info": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", + "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "html-tags": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", + "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", + "dev": true + }, + "import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "is-regexp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz", + "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "known-css-properties": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.21.0.tgz", + "integrity": "sha512-sZLUnTqimCkvkgRS+kbPlYW5o8q5w1cu+uIisKpEWkj31I8mx8kNG162DwRav8Zirkva6N5uoFsm9kzK4mUXjw==", + "dev": true + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "map-obj": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", + "integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==", + "dev": true + }, + "meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + } + }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + } + }, + "normalize-package-data": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.2.tgz", + "integrity": "sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "resolve": "^1.20.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "postcss-safe-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz", + "integrity": "sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==", + "dev": true, + "requires": { + "postcss": "^7.0.26" + } + }, + "postcss-sass": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.4.4.tgz", + "integrity": "sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg==", + "dev": true, + "requires": { + "gonzales-pe": "^4.3.0", + "postcss": "^7.0.21" + } + }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "table": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.9.tgz", + "integrity": "sha512-F3cLs9a3hL1Z7N4+EkSscsel3z55XT950AvB05bwayrNg5T1/gykXtigioTAjbltvbMSJvvhFCbnf6mX+ntnJQ==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "lodash.clonedeep": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" + } + }, + "trim-newlines": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", + "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", + "dev": true + }, + "type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + }, + "yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "dev": true + } + } + }, + "stylelint-config-prettier": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/stylelint-config-prettier/-/stylelint-config-prettier-8.0.2.tgz", + "integrity": "sha512-TN1l93iVTXpF9NJstlvP7nOu9zY2k+mN0NSFQ/VEGz15ZIP9ohdDZTtCWHs5LjctAhSAzaILULGbgiM0ItId3A==", + "dev": true + }, + "stylelint-config-rational-order": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/stylelint-config-rational-order/-/stylelint-config-rational-order-0.1.2.tgz", + "integrity": "sha512-Qo7ZQaihCwTqijfZg4sbdQQHtugOX/B1/fYh018EiDZHW+lkqH9uHOnsDwDPGZrYJuB6CoyI7MZh2ecw2dOkew==", + "dev": true, + "requires": { + "stylelint": "^9.10.1", + "stylelint-order": "^2.2.1" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "globby": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", + "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^1.0.2", + "dir-glob": "^2.2.2", + "fast-glob": "^2.2.6", + "glob": "^7.1.3", + "ignore": "^4.0.3", + "pify": "^4.0.1", + "slash": "^2.0.0" + }, + "dependencies": { + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", + "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0", + "yargs-parser": "^10.0.0" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "postcss-safe-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz", + "integrity": "sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==", + "dev": true, + "requires": { + "postcss": "^7.0.26" + } + }, + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "stylelint": { + "version": "9.10.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-9.10.1.tgz", + "integrity": "sha512-9UiHxZhOAHEgeQ7oLGwrwoDR8vclBKlSX7r4fH0iuu0SfPwFaLkb1c7Q2j1cqg9P7IDXeAV2TvQML/fRQzGBBQ==", + "dev": true, + "requires": { + "autoprefixer": "^9.0.0", + "balanced-match": "^1.0.0", + "chalk": "^2.4.1", + "cosmiconfig": "^5.0.0", + "debug": "^4.0.0", + "execall": "^1.0.0", + "file-entry-cache": "^4.0.0", + "get-stdin": "^6.0.0", + "global-modules": "^2.0.0", + "globby": "^9.0.0", + "globjoin": "^0.1.4", + "html-tags": "^2.0.0", + "ignore": "^5.0.4", + "import-lazy": "^3.1.0", + "imurmurhash": "^0.1.4", + "known-css-properties": "^0.11.0", + "leven": "^2.1.0", + "lodash": "^4.17.4", + "log-symbols": "^2.0.0", + "mathml-tag-names": "^2.0.1", + "meow": "^5.0.0", + "micromatch": "^3.1.10", + "normalize-selector": "^0.2.0", + "pify": "^4.0.0", + "postcss": "^7.0.13", + "postcss-html": "^0.36.0", + "postcss-jsx": "^0.36.0", + "postcss-less": "^3.1.0", + "postcss-markdown": "^0.36.0", + "postcss-media-query-parser": "^0.2.3", + "postcss-reporter": "^6.0.0", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-safe-parser": "^4.0.0", + "postcss-sass": "^0.3.5", + "postcss-scss": "^2.0.0", + "postcss-selector-parser": "^3.1.0", + "postcss-syntax": "^0.36.2", + "postcss-value-parser": "^3.3.0", + "resolve-from": "^4.0.0", + "signal-exit": "^3.0.2", + "slash": "^2.0.0", + "specificity": "^0.4.1", + "string-width": "^3.0.0", + "style-search": "^0.1.0", + "sugarss": "^2.0.0", + "svg-tags": "^1.0.0", + "table": "^5.0.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "stylelint-config-recommended": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz", + "integrity": "sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ==", + "dev": true + }, + "stylelint-config-standard": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-20.0.0.tgz", + "integrity": "sha512-IB2iFdzOTA/zS4jSVav6z+wGtin08qfj+YyExHB3LF9lnouQht//YyB0KZq9gGz5HNPkddHOzcY8HsUey6ZUlA==", + "dev": true, + "requires": { + "stylelint-config-recommended": "^3.0.0" + } + }, + "stylelint-order": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/stylelint-order/-/stylelint-order-2.2.1.tgz", + "integrity": "sha512-019KBV9j8qp1MfBjJuotse6MgaZqGVtXMc91GU9MsS9Feb+jYUvUU3Z8XiClqPdqJZQ0ryXQJGg3U3PcEjXwfg==", + "dev": true, + "requires": { + "lodash": "^4.17.10", + "postcss": "^7.0.2", + "postcss-sorting": "^4.1.0" + } + }, "subscriptions-transport-ws": { "version": "0.9.18", "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.18.tgz", @@ -17135,6 +18474,15 @@ } } }, + "sugarss": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz", + "integrity": "sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -17172,6 +18520,12 @@ "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" }, + "svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", + "dev": true + }, "svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", @@ -17212,36 +18566,54 @@ } }, "table": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/table/-/table-6.0.9.tgz", - "integrity": "sha512-F3cLs9a3hL1Z7N4+EkSscsel3z55XT950AvB05bwayrNg5T1/gykXtigioTAjbltvbMSJvvhFCbnf6mX+ntnJQ==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, "requires": { - "ajv": "^8.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "lodash.clonedeep": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.0" + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" }, "dependencies": { - "ajv": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.1.0.tgz", - "integrity": "sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ==", + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" } }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, @@ -17535,18 +18907,23 @@ "punycode": "^2.1.1" } }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + "trim": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", + "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", + "dev": true }, - "true-case-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", - "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", - "requires": { - "glob": "^7.1.2" - } + "trim-trailing-lines": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz", + "integrity": "sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==", + "dev": true + }, + "trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "dev": true }, "tryer": { "version": "1.0.1", @@ -17681,6 +19058,16 @@ "which-boxed-primitive": "^1.0.2" } }, + "unherit": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", + "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", + "dev": true, + "requires": { + "inherits": "^2.0.0", + "xtend": "^4.0.0" + } + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -17705,6 +19092,22 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==" }, + "unified": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-7.1.0.tgz", + "integrity": "sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "@types/vfile": "^3.0.0", + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^1.1.0", + "trough": "^1.0.0", + "vfile": "^3.0.0", + "x-is-string": "^0.1.0" + } + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -17750,6 +19153,57 @@ "crypto-random-string": "^1.0.0" } }, + "unist-util-find-all-after": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-1.0.5.tgz", + "integrity": "sha512-lWgIc3rrTMTlK1Y0hEuL+k+ApzFk78h+lsaa2gHf63Gp5Ww+mt11huDniuaoq1H+XMK2lIIjjPkncxXcDp3QDw==", + "dev": true, + "requires": { + "unist-util-is": "^3.0.0" + } + }, + "unist-util-is": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", + "dev": true + }, + "unist-util-remove-position": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz", + "integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==", + "dev": true, + "requires": { + "unist-util-visit": "^1.1.0" + } + }, + "unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dev": true, + "requires": { + "@types/unist": "^2.0.2" + } + }, + "unist-util-visit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", + "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "dev": true, + "requires": { + "unist-util-visit-parents": "^2.0.0" + } + }, + "unist-util-visit-parents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", + "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "dev": true, + "requires": { + "unist-util-is": "^3.0.0" + } + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -17996,6 +19450,57 @@ "extsprintf": "^1.2.0" } }, + "vfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-3.0.1.tgz", + "integrity": "sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==", + "dev": true, + "requires": { + "is-buffer": "^2.0.0", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^1.0.0", + "vfile-message": "^1.0.0" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true + }, + "unist-util-stringify-position": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", + "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==", + "dev": true + }, + "vfile-message": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz", + "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", + "dev": true, + "requires": { + "unist-util-stringify-position": "^1.1.1" + } + } + } + }, + "vfile-location": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz", + "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==", + "dev": true + }, + "vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + } + }, "vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -19152,43 +20657,6 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -19473,6 +20941,15 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, "write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", @@ -19489,6 +20966,12 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==" }, + "x-is-string": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", + "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", + "dev": true + }, "xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", @@ -19555,6 +21038,12 @@ "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "zwitch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", + "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", + "dev": true } } } diff --git a/client/package.json b/client/package.json index 7515608..77e254c 100644 --- a/client/package.json +++ b/client/package.json @@ -38,10 +38,12 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "lint": "eslint src/ --fix; stylelint src/**/*.css --fix; prettier src/ --write" }, - "eslintConfig": { - "extends": "react-app" + "prettier": "@hex-labs/prettier-config", + "stylelint": { + "extends": "@hex-labs/stylelint-config" }, "browserslist": [ ">0.2%", @@ -51,6 +53,9 @@ ], "author": "Evan Strat", "devDependencies": { + "@hex-labs/eslint-config-react": "^1.1.1", + "@hex-labs/prettier-config": "^1.1.1", + "@hex-labs/stylelint-config": "^1.1.1", "@types/accounting": "^0.4.1", "@types/cleave.js": "^1.4.4", "@types/jest": "26.0.22", @@ -60,7 +65,10 @@ "@types/react-router-dom": "^5.1.7", "@types/react-timeago": "^4.1.2", "@types/semantic-ui": "^2.2.7", - "redux-devtools": "^3.5.0" + "eslint": "^7.24.0", + "prettier": "^2.2.1", + "redux-devtools": "^3.5.0", + "stylelint": "^13.12.0" }, "proxy": "http://localhost:3000" } diff --git a/client/src/App.test.tsx b/client/src/App.test.tsx index a754b20..f7d70fe 100644 --- a/client/src/App.test.tsx +++ b/client/src/App.test.tsx @@ -1,9 +1,10 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; +import React from "react"; +import ReactDOM from "react-dom"; -it('renders without crashing', () => { - const div = document.createElement('div'); +import App from "./App"; + +it("renders without crashing", () => { + const div = document.createElement("div"); ReactDOM.render(, div); ReactDOM.unmountComponentAtNode(div); }); diff --git a/client/src/App.tsx b/client/src/App.tsx index 56e3aaa..dc47677 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,22 +1,23 @@ -import React, {Component} from "react"; +import React, { Component } from "react"; +import { ToastProvider } from "react-toast-notifications"; +import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; +import { connect } from "react-redux"; + import Navigation from "./components/Navigation"; import Footer from "./components/Footer"; -import {ToastProvider} from "react-toast-notifications"; import HomeContainer from "./components/HomeContainer"; -import {BrowserRouter as Router, Route, Switch} from "react-router-dom"; import CSVWizard from "./components/csv/CSVWizard"; import ItemWrapper from "./components/item/ItemWrapper"; -import {connect} from "react-redux"; import PrivateRoute from "./components/util/PrivateRoute"; import AdminOverviewContainer from "./components/admin/AdminOverviewContainer"; -import {bugsnagClient, bugsnagEnabled} from "./index"; +import { bugsnagClient, bugsnagEnabled } from "./index"; import AdminUsersListWrapper from "./components/admin/AdminUsersListWrapper"; import AdminRequestsWrapper from "./components/admin/AdminRequestsWrapper"; import UserProfileWrapper from "./components/users/UserProfileWrapper"; import DeskContainer from "./components/desk/DeskContainer"; -import {User} from "./types/User"; -import {loginUser} from "./state/Account"; -import {AppState} from "./state/Store"; +import { User } from "./types/User"; +import { loginUser } from "./state/Account"; +import { AppState } from "./state/Store"; import CacheBuster from "./components/util/CacheBuster"; import DetailedItemStatistics from "./components/reports/statistics/DetailedItemStatistics"; import ItemDemandReport from "./components/reports/demand/ItemDemandReport"; @@ -24,22 +25,22 @@ import ItemDemandReport from "./components/reports/demand/ItemDemandReport"; export interface OwnProps {} interface StateProps { - user: User|null; - loginUser: (user: User) => void; + user: User | null; + loginUser: (user: User) => void; } type Props = StateProps & OwnProps; class App extends Component { - public async componentWillMount(): Promise { - const userRequest = await fetch("/api", { - credentials: "include", - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: ` + public async componentWillMount(): Promise { + const userRequest = await fetch("/api", { + credentials: "include", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: ` query { user { uuid @@ -47,88 +48,91 @@ class App extends Component { admin } } - ` - }), - }); - const json = await userRequest.json(); - if (json && json.data && json.data.user) { - const user = json.data.user; - if (user) { - this.props.loginUser(user); - if (bugsnagEnabled) { - bugsnagClient.user = user; - } - } - } else { - console.error("Invalid user information returned by server, can't sign in: ", json); - if (bugsnagEnabled) { - bugsnagClient.notify("Invalid user information returned by server, can't sign in", { - severity: "error", - metaData: { - fetchResult: json - } - }); - } + `, + }), + }); + const json = await userRequest.json(); + if (json && json.data && json.data.user) { + const { user } = json.data; + if (user) { + this.props.loginUser(user); + if (bugsnagEnabled) { + bugsnagClient.user = user; } + } + } else { + console.error("Invalid user information returned by server, can't sign in: ", json); + if (bugsnagEnabled) { + bugsnagClient.notify("Invalid user information returned by server, can't sign in", { + severity: "error", + metaData: { + fetchResult: json, + }, + }); + } } + } - - public render() { - return ( -
- - - - - - - - - - - - - - - - -
- - - - {({loading, isLatestVersion, refreshCacheAndReload}: any) => { - if (loading) { - return null; - } - if (!loading && !isLatestVersion) { - // You can decide how and when you want to force reload - refreshCacheAndReload(); - } - return null; - }} - -
- ); - } + public render() { + return ( +
+ + + + + + + + + + + + + + + + +
+ + + + {({ loading, isLatestVersion, refreshCacheAndReload }: any) => { + if (loading) { + return null; + } + if (!loading && !isLatestVersion) { + // You can decide how and when you want to force reload + refreshCacheAndReload(); + } + return null; + }} + +
+ ); + } } function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } -const mapDispatchToProps = (dispatch: any) => { - return { - loginUser: (user: User) => { - dispatch(loginUser(user)); - } - }; -}; +const mapDispatchToProps = (dispatch: any) => ({ + loginUser: (user: User) => { + dispatch(loginUser(user)); + }, +}); export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/client/src/components/FeedbackLink.tsx b/client/src/components/FeedbackLink.tsx index 09d94be..7773143 100644 --- a/client/src/components/FeedbackLink.tsx +++ b/client/src/components/FeedbackLink.tsx @@ -1,30 +1,34 @@ -import React from 'react'; -import SlackFeedback from 'react-slack-feedback'; +import React from "react"; +import SlackFeedback from "react-slack-feedback"; const FeedbackLink = () => { - // @ts-ignore - function sendToServer(payload, success, error) { - return fetch('/api/slack/feedback', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload.attachments[0]) - }) + // @ts-ignore + function sendToServer(payload, success, error) { + return fetch("/api/slack/feedback", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload.attachments[0]), + }) .then(success) - .catch(error) - } + .catch(error); + } - return + return ( + ; + onSubmit={(payload, success, error) => + // @ts-ignore + sendToServer(payload).then(success).catch(error) + } + /> + ); }; export default FeedbackLink; diff --git a/client/src/components/Footer.tsx b/client/src/components/Footer.tsx index b8a0809..4bb9c3f 100644 --- a/client/src/components/Footer.tsx +++ b/client/src/components/Footer.tsx @@ -1,19 +1,25 @@ import * as React from "react"; + import FeedbackLink from "./FeedbackLink"; import packageJson from "../../package.json"; -const Footer: React.FunctionComponent<{}> = (props) => { - return ( -
-

Made with 🤖 by the HackGTeam - HackGT Hardware Checkout is - powered by Bolt v{packageJson.version}

- -
- ); -}; +const Footer: React.FunctionComponent<{}> = props => ( +
+

+ Made with{" "} + + 🤖 + {" "} + by the HackGTeam - HackGT Hardware Checkout is powered by{" "} + Bolt v{packageJson.version} +

+ +
+); export default Footer; diff --git a/client/src/components/HomeContainer.tsx b/client/src/components/HomeContainer.tsx index af6c7b1..884aad6 100644 --- a/client/src/components/HomeContainer.tsx +++ b/client/src/components/HomeContainer.tsx @@ -1,79 +1,82 @@ -import React, {Component} from "react"; -import {Grid, Segment} from "semantic-ui-react"; +import React, { Component } from "react"; +import { Grid, Segment } from "semantic-ui-react"; +import { connect } from "react-redux"; + import RequestedList from "./requests/RequestedList"; -import {connect} from "react-redux"; -import {User} from "../types/User"; -import {AppState} from "../state/Store"; -import {RequestedItem} from "../types/Hardware"; +import { User } from "../types/User"; +import { AppState } from "../state/Store"; +import { RequestedItem } from "../types/Hardware"; import NewHardwareList from "./inventory/NewHardwareList"; -export interface OwnProps { -} +export interface OwnProps {} interface StateProps { - user: User | null; + user: User | null; } -type Props = StateProps & OwnProps | any; +type Props = (StateProps & OwnProps) | any; interface State { - requestedItemsList: RequestedItem[]; - item: RequestedItem | null; + requestedItemsList: RequestedItem[]; + item: RequestedItem | null; } class HomeContainer extends Component { - constructor(props: Props) { - super(props); - this.state = { - requestedItemsList: [] as RequestedItem[], - item: {} as RequestedItem | null - }; - } - - public handleAddItem = (item: RequestedItem) => { - const listOfItems: RequestedItem[] = this.state.requestedItemsList; - listOfItems.push(item); - this.setState({ - requestedItemsList: listOfItems - }); + constructor(props: Props) { + super(props); + this.state = { + requestedItemsList: [] as RequestedItem[], + item: {} as RequestedItem | null, }; + } - public handleRemoveItem = (index: number) => { - const listOfItems: RequestedItem[] = this.state.requestedItemsList; - const itemToAddBack: RequestedItem = listOfItems[index]; - listOfItems.splice(index, 1); - this.setState({ - requestedItemsList: listOfItems, - item: itemToAddBack - }); - }; + public handleAddItem = (item: RequestedItem) => { + const listOfItems: RequestedItem[] = this.state.requestedItemsList; + listOfItems.push(item); + this.setState({ + requestedItemsList: listOfItems, + }); + }; - public render() { - const myRequests = this.props.user ? ( -

My Requests

- - - -
) : ""; + public handleRemoveItem = (index: number) => { + const listOfItems: RequestedItem[] = this.state.requestedItemsList; + const itemToAddBack: RequestedItem = listOfItems[index]; + listOfItems.splice(index, 1); + this.setState({ + requestedItemsList: listOfItems, + item: itemToAddBack, + }); + }; - return ( - - - - - - {myRequests} - - - ); - } + public render() { + const myRequests = this.props.user ? ( + +

My Requests

+ + + +
+ ) : ( + "" + ); + + return ( + + + + + + {myRequests} + + + ); + } } function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } - export default connect(mapStateToProps)(HomeContainer); diff --git a/client/src/components/Navigation.tsx b/client/src/components/Navigation.tsx index e01f5cf..d225220 100644 --- a/client/src/components/Navigation.tsx +++ b/client/src/components/Navigation.tsx @@ -1,67 +1,79 @@ import React from "react"; -import {Icon, Menu, Popup} from "semantic-ui-react"; -import {Link} from "react-router-dom"; -import {connect} from "react-redux"; -import {User} from "../types/User"; -import {AppState} from "../state/Store"; +import { Icon, Menu, Popup } from "semantic-ui-react"; +import { Link } from "react-router-dom"; +import { connect } from "react-redux"; + +import { User } from "../types/User"; +import { AppState } from "../state/Store"; export interface OwnProps {} interface StateProps { - user: User|null; + user: User | null; } type Props = StateProps & OwnProps; class Navigation extends React.Component { - public render() { - const { user } = this.props; - - const homeLink = ( - - Inventory - - ); + public render() { + const { user } = this.props; - const loginLink = !user && Sign in; + const homeLink = ( + + + + Inventory + + + ); - const userProfile = user && - {user.name}} - content="Edit your profile"/>; + const loginLink = !user && Sign in; - const adminLink = this.isAdmin() && - - Admin - ; + const userProfile = user && ( + + {user.name} + + } + content="Edit your profile" + /> + ); - const logoutLink = user && Sign out; + const adminLink = this.isAdmin() && ( + + + Admin + + ); - return ( - - HackGT Hardware - {homeLink} - {adminLink} - {loginLink} - - {userProfile} - {logoutLink} - - + const logoutLink = user && Sign out; - ); - } + return ( + + HackGT Hardware + {homeLink} + {adminLink} + {loginLink} + + {userProfile} + {logoutLink} + + + ); + } - private isAdmin = () => { - const {user} = this.props; - return user && user.admin; - } + private isAdmin = () => { + const { user } = this.props; + return user && user.admin; + }; } function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } -export default connect(mapStateToProps) (Navigation); +export default connect(mapStateToProps)(Navigation); diff --git a/client/src/components/admin/AdminLinksCard.tsx b/client/src/components/admin/AdminLinksCard.tsx index 8f59f28..219d5a8 100644 --- a/client/src/components/admin/AdminLinksCard.tsx +++ b/client/src/components/admin/AdminLinksCard.tsx @@ -1,46 +1,51 @@ import React from "react"; -import {connect} from "react-redux"; -import {Card, Header, Label, List} from "semantic-ui-react"; -import {Link} from "react-router-dom"; -import {AppState} from "../../state/Store"; -import {AdminCardLink} from "./AdminOverviewContainer"; +import { connect } from "react-redux"; +import { Card, Header, Label, List } from "semantic-ui-react"; +import { Link } from "react-router-dom"; + +import { AppState } from "../../state/Store"; +import { AdminCardLink } from "./AdminOverviewContainer"; export interface OwnProps { - title: string; - links: AdminCardLink[]; - notice?: string; + title: string; + links: AdminCardLink[]; + notice?: string; } -interface StateProps { -} +interface StateProps {} type Props = StateProps & OwnProps; function AdminLinksCard(props: Props) { - const content = ( - - {props.links.map(value => ( - - {value.external ? {value.name} - : {value.name}} - ))} - ); - return ( - - -
{props.title} {props.notice ? : ""}
- {content} -
-
- ); + const content = ( + + {props.links.map(value => ( + + {value.external ? ( + {value.name} + ) : ( + {value.name} + )} + + ))} + + ); + return ( + + +
+ {props.title} {props.notice ? : ""} +
+ {content} +
+
+ ); } function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } -export default connect( - mapStateToProps -)(AdminLinksCard); +export default connect(mapStateToProps)(AdminLinksCard); diff --git a/client/src/components/admin/AdminOverviewContainer.tsx b/client/src/components/admin/AdminOverviewContainer.tsx index 8f11068..a59710f 100644 --- a/client/src/components/admin/AdminOverviewContainer.tsx +++ b/client/src/components/admin/AdminOverviewContainer.tsx @@ -1,89 +1,87 @@ -import React, {useState} from "react"; -import {connect} from "react-redux"; -import {AppState} from "../../state/Store"; +import React, { useState } from "react"; +import { connect } from "react-redux"; +import { Grid, Header } from "semantic-ui-react"; +import { randomItemString } from "stuff-with-good-eyesight"; + +import { AppState } from "../../state/Store"; import AdminLinksCard from "./AdminLinksCard"; -import {Grid, Header} from "semantic-ui-react"; -import {randomItemString} from "stuff-with-good-eyesight"; export type AdminCardLink = { - name: string; - to: string; - external: boolean; + name: string; + to: string; + external: boolean; }; -function adminCardLink(name: string, to: string = "#", external: boolean = false): AdminCardLink { - return {name, to, external}; +function adminCardLink(name: string, to = "#", external = false): AdminCardLink { + return { name, to, external }; } -const hardwareDesk: AdminCardLink[] = [ - adminCardLink("Work hardware desk", "/admin/desk"), -]; +const hardwareDesk: AdminCardLink[] = [adminCardLink("Work hardware desk", "/admin/desk")]; const manage: AdminCardLink[] = [ - adminCardLink("Users", "/admin/users"), - adminCardLink("Request settings", "/admin/requests") + adminCardLink("Users", "/admin/users"), + adminCardLink("Request settings", "/admin/requests"), ]; const reports: AdminCardLink[] = [ - adminCardLink("Detailed item statistics", "/admin/reports/statistics"), - adminCardLink("Item demand", "/admin/reports/demand") + adminCardLink("Detailed item statistics", "/admin/reports/statistics"), + adminCardLink("Item demand", "/admin/reports/demand"), ]; -const baseUrl = (process.env.NODE_ENV === "production") ? "" : "http://localhost:3000"; +const baseUrl = process.env.NODE_ENV === "production" ? "" : "http://localhost:3000"; const utilities: AdminCardLink[] = [ - adminCardLink("Import items", "/admin/csv"), - adminCardLink("GraphiQL", `${baseUrl}/api/graphiql`, true) + adminCardLink("Import items", "/admin/csv"), + adminCardLink("GraphiQL", `${baseUrl}/api/graphiql`, true), ]; const funPhrases: string[] = [ - "Congrats, you made it to the big leagues!", - "The coolest part of Bolt", - "Links, links, getcha links here!", - "Hello, friendly administrator", - "Use your power wisely", - "Nice to see you", - "It's a wonderful day to configure Bolt", - "Millions of hardware items look up to you", - "We go together like a nut and a bolt", - "Thank you for calling the Ellie Morton Water Bottle Company, how may I help you?", - "Did you know: Bolt is held together with 1,482 bolts", - "A developer somewhere spent multiple minutes adding these random phrases", - "Did you know: a robot personally prepares this page for you each time you view it", - "Releasing a new version of Bolt just to update these messages would be a real power move", - `"${randomItemString()}" -James Lu` + "Congrats, you made it to the big leagues!", + "The coolest part of Bolt", + "Links, links, getcha links here!", + "Hello, friendly administrator", + "Use your power wisely", + "Nice to see you", + "It's a wonderful day to configure Bolt", + "Millions of hardware items look up to you", + "We go together like a nut and a bolt", + "Thank you for calling the Ellie Morton Water Bottle Company, how may I help you?", + "Did you know: Bolt is held together with 1,482 bolts", + "A developer somewhere spent multiple minutes adding these random phrases", + "Did you know: a robot personally prepares this page for you each time you view it", + "Releasing a new version of Bolt just to update these messages would be a real power move", + `"${randomItemString()}" -James Lu`, ]; export function pickRandomElement(arr: T[]): T { - return arr[Math.floor(Math.random() * arr.length)]; + return arr[Math.floor(Math.random() * arr.length)]; } function AdminOverviewContainer() { - const randomPhrase = useState(pickRandomElement(funPhrases)); - return ( - - - -
Administration - {randomPhrase} -
-
- - - - -
-
-
-
- ); + const randomPhrase = useState(pickRandomElement(funPhrases)); + return ( + + + +
+ Administration + {randomPhrase} +
+
+ + + + +
+
+
+
+ ); } function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } -export default connect( - mapStateToProps -)(AdminOverviewContainer); +export default connect(mapStateToProps)(AdminOverviewContainer); diff --git a/client/src/components/admin/AdminRequestsWrapper.tsx b/client/src/components/admin/AdminRequestsWrapper.tsx index a21b09b..eafb11f 100644 --- a/client/src/components/admin/AdminRequestsWrapper.tsx +++ b/client/src/components/admin/AdminRequestsWrapper.tsx @@ -1,79 +1,92 @@ -import React, {Component} from "react"; -import {connect} from "react-redux"; -import {AppState} from "../../state/Store"; -import {Button, Checkbox, Grid, Header, Loader, Message} from "semantic-ui-react"; -import {Query, Mutation} from "@apollo/client/react/components"; -import {GET_SETTING} from "../util/graphql/Queries"; -import {UPDATE_SETTING, CREATE_SETTING} from "../util/graphql/Mutations"; +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { Button, Checkbox, Grid, Header, Loader, Message } from "semantic-ui-react"; +import { Query, Mutation } from "@apollo/client/react/components"; + +import { AppState } from "../../state/Store"; +import { GET_SETTING } from "../util/graphql/Queries"; +import { UPDATE_SETTING, CREATE_SETTING } from "../util/graphql/Mutations"; class AdminRequestsWrapper extends Component { - public render() { - let requests_allowed = false; - return
-
Requests
- - { - ({loading, error, data}: any) => { - if (loading) { - return ; - } - if (error) { - return
- - - {(createSetting: any, {loading, data}: any) => ( -
; - } - if(data.setting !== undefined) { - requests_allowed = (data.setting.value === "true"); - } - return ( - - - - - {(updateSetting: any, {loading, data}: any) => ( - { - updateSetting({variables: {settingName: "requests_allowed", updatedSetting: {name: "requests_allowed", value: checked ? "true" : "false" }}}); - requests_allowed = !requests_allowed; - })} - /> - )} - - - - - ) - } - } -
-
; - } + public render() { + let requests_allowed = false; + return ( +
+
Requests
+ + {({ loading, error, data }: any) => { + if (loading) { + return ; + } + if (error) { + return ( +
+ + + {(createSetting: any, { loading, data }: any) => ( +
+ ); + } + if (data.setting !== undefined) { + requests_allowed = data.setting.value === "true"; + } + return ( + + + + + {(updateSetting: any, { loading, data }: any) => ( + { + updateSetting({ + variables: { + settingName: "requests_allowed", + updatedSetting: { + name: "requests_allowed", + value: checked ? "true" : "false", + }, + }, + }); + requests_allowed = !requests_allowed; + }} + /> + )} + + + + + ); + }} +
+
+ ); + } } function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } -export default connect( - mapStateToProps -)(AdminRequestsWrapper); +export default connect(mapStateToProps)(AdminRequestsWrapper); diff --git a/client/src/components/admin/AdminUsersListTable.tsx b/client/src/components/admin/AdminUsersListTable.tsx index d9099df..91958c2 100644 --- a/client/src/components/admin/AdminUsersListTable.tsx +++ b/client/src/components/admin/AdminUsersListTable.tsx @@ -1,201 +1,233 @@ -import React, {Component} from "react"; -import {connect} from "react-redux"; -import {AppState} from "../../state/Store"; -import {Button, Icon, Input, Table, TableHeaderCell} from "semantic-ui-react"; -import {Mutation} from "@apollo/client/react/components"; -import {Link} from "react-router-dom"; -import {withToastManager} from "react-toast-notifications"; -import {FullUser, User} from "../../types/User"; -import {ALL_USERS} from "../util/graphql/Queries"; -import {UPDATE_USER} from "../util/graphql/Mutations"; +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { Button, Icon, Input, Table, TableHeaderCell } from "semantic-ui-react"; +import { Mutation } from "@apollo/client/react/components"; +import { Link } from "react-router-dom"; +import { withToastManager } from "react-toast-notifications"; + +import { AppState } from "../../state/Store"; +import { FullUser, User } from "../../types/User"; +import { ALL_USERS } from "../util/graphql/Queries"; +import { UPDATE_USER } from "../util/graphql/Mutations"; type UsersListProps = { - users: FullUser[]; - toastManager: any; + users: FullUser[]; + toastManager: any; }; interface StateProps { - user: User | null; + user: User | null; } type UsersListState = { - users: FullUser[]; - loadingUsers: { - [key: string]: boolean - }; - searchQuery: string; + users: FullUser[]; + loadingUsers: { + [key: string]: boolean; + }; + searchQuery: string; }; type Props = UsersListProps & StateProps; class AdminUsersListTable extends Component { - private static checkOrX(value: boolean) { - if (value) { - return ; - } - - return ; - } - - constructor(props: Props) { - super(props); - this.state = { - users: props.users, - loadingUsers: {}, - searchQuery: "" - }; + private static checkOrX(value: boolean) { + if (value) { + return ; } - public render = () => { - const tableColNames = ["Name", "Email", "Phone", "Slack Username", "Have ID", "Admin", "Actions"]; - const tableCols = tableColNames.map(col => ( - {col} - )); + return ; + } - let tableRows = (changeAdmin: any) => this.state.users.filter((user => this.containsSearchQuery(user))).map(user => ( - - {user.name} - {user.email} - {user.phone} - {user.slackUsername} - {AdminUsersListTable.checkOrX(user.haveID)} - {AdminUsersListTable.checkOrX(user.admin)} - {this.adminButton(user, changeAdmin)} - + constructor(props: Props) { + super(props); + this.state = { + users: props.users, + loadingUsers: {}, + searchQuery: "", + }; + } + + public render = () => { + const tableColNames = [ + "Name", + "Email", + "Phone", + "Slack Username", + "Have ID", + "Admin", + "Actions", + ]; + const tableCols = tableColNames.map(col => ( + + {col} + + )); + + let tableRows = (changeAdmin: any) => + this.state.users + .filter(user => this.containsSearchQuery(user)) + .map(user => ( + + {user.name} + + {user.email} + + {user.phone} + {user.slackUsername} + {AdminUsersListTable.checkOrX(user.haveID)} + {AdminUsersListTable.checkOrX(user.admin)} + + {this.adminButton(user, changeAdmin)}{" "} + + + )); - - if (!tableRows(null).length) { - tableRows = (changeAdmin: any) => [ - No users found - ]; - } - - return ( - - {(changeAdmin: any, {loading, data}: any) => ( -
- { - // @ts-ignore - this.setState({ - searchQuery: value.trim().toLowerCase() - }); - } - } - /> - - - - {tableCols} - - - - {tableRows(changeAdmin)} - -
-
- )} -
); + if (!tableRows(null).length) { + tableRows = (changeAdmin: any) => [ + + No users found + , + ]; } - private containsSearchQuery(user: FullUser) { - const query: string = this.state.searchQuery; - return user.name.toLowerCase().indexOf(query) !== -1 - || user.email.toLowerCase().indexOf(query) !== -1 - || user.phone.indexOf(query) !== -1 - || user.slackUsername.indexOf(query) !== -1 - || user.uuid.indexOf(query) !== -1; - } - - private getUserIndex(uuid: string) { - return this.state.users.findIndex((user) => user.uuid === uuid); - } + return ( + + {(changeAdmin: any, { loading, data }: any) => ( +
+ { + // @ts-ignore + this.setState({ + searchQuery: value.trim().toLowerCase(), + }); + }} + /> + + + {tableCols} + + {tableRows(changeAdmin)} +
+
+ )} +
+ ); + }; + + private containsSearchQuery(user: FullUser) { + const query: string = this.state.searchQuery; + return ( + user.name.toLowerCase().indexOf(query) !== -1 || + user.email.toLowerCase().indexOf(query) !== -1 || + user.phone.indexOf(query) !== -1 || + user.slackUsername.indexOf(query) !== -1 || + user.uuid.indexOf(query) !== -1 + ); + } + + private getUserIndex(uuid: string) { + return this.state.users.findIndex(user => user.uuid === uuid); + } + + private modifyUserAdminInState = (uuid: string, admin: boolean) => { + const userIndex = this.getUserIndex(uuid); + const { users } = this.state; + + users[userIndex] = { + ...users[userIndex], + admin, + }; - private modifyUserAdminInState = (uuid: string, admin: boolean) => { - const userIndex = this.getUserIndex(uuid); - const users = this.state.users; + this.setState({ + users, + }); + }; - users[userIndex] = { - ...users[userIndex], - admin - }; + private modifyLoadingUsers = (uuid: string, loading: boolean) => { + const updatedLoadingUsers = this.state.loadingUsers; + updatedLoadingUsers[uuid] = loading; - this.setState({ - users - }); - } + this.setState({ + loadingUsers: updatedLoadingUsers, + }); + }; - private modifyLoadingUsers = (uuid: string, loading: boolean) => { - const updatedLoadingUsers = this.state.loadingUsers; - updatedLoadingUsers[uuid] = loading; + private adminButton = (user: FullUser, changeAdmin: any, loading = true) => { + const { name, uuid, admin } = user; - this.setState({ - loadingUsers: updatedLoadingUsers - }); - } - - private adminButton = (user: FullUser, changeAdmin: any, loading: boolean = true) => { - const {name, uuid, admin} = user; - - if (this.props.user && uuid === this.props.user.uuid) { - return ""; - } - // flip admin value so we promote/demote this user - const newAdminValue = !admin; - return ; + if (this.props.user && uuid === this.props.user.uuid) { + return ""; } + // flip admin value so we promote/demote this user + const newAdminValue = !admin; + return ( + + ); + }; } function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } // Using compose here doesn't work. Note the structure of this: withToastManager( connect()(component) ) diff --git a/client/src/components/admin/AdminUsersListWrapper.tsx b/client/src/components/admin/AdminUsersListWrapper.tsx index 54ca639..dd6cf57 100644 --- a/client/src/components/admin/AdminUsersListWrapper.tsx +++ b/client/src/components/admin/AdminUsersListWrapper.tsx @@ -1,49 +1,47 @@ -import React, {Component} from "react"; -import {connect} from "react-redux"; -import {AppState} from "../../state/Store"; -import {Header, Loader, Message} from "semantic-ui-react"; -import {Query} from "@apollo/client/react/components"; -import AdminUsersListTable from "./AdminUsersListTable"; -import {FullUser} from "../../types/User"; -import {ALL_USERS} from "../util/graphql/Queries"; +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { Header, Loader, Message } from "semantic-ui-react"; +import { Query } from "@apollo/client/react/components"; +import { AppState } from "../../state/Store"; +import AdminUsersListTable from "./AdminUsersListTable"; +import { FullUser } from "../../types/User"; +import { ALL_USERS } from "../util/graphql/Queries"; class AdminUsersListWrapper extends Component { - public render() { - - return
-
Users
- - { - ({loading, error, data}: any) => { - if (loading) { - return ; - } - if (error) { - return ; - } - const {users}: { users: FullUser[] } = data; + public render() { + return ( +
+
Users
+ + {({ loading, error, data }: any) => { + if (loading) { + return ; + } + if (error) { + return ( + + ); + } + const { users }: { users: FullUser[] } = data; - return ; - } - } - -
; - } + return ; + }} +
+
+ ); + } } function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } -export default connect( - mapStateToProps -)(AdminUsersListWrapper); +export default connect(mapStateToProps)(AdminUsersListWrapper); diff --git a/client/src/components/csv/CSVReview.tsx b/client/src/components/csv/CSVReview.tsx index 263d3cb..e6080d7 100644 --- a/client/src/components/csv/CSVReview.tsx +++ b/client/src/components/csv/CSVReview.tsx @@ -1,52 +1,77 @@ import React from "react"; -import {Container, Item, Label} from "semantic-ui-react"; -import {ItemComplete} from "../item/ItemEditForm"; +import { Container, Item, Label } from "semantic-ui-react"; + +import { ItemComplete } from "../item/ItemEditForm"; interface ReviewCardProps { - item: ItemComplete; + item: ItemComplete; } interface ReviewProps { - inventory: ItemComplete[]; + inventory: ItemComplete[]; } const ReviewSetup = (props: ReviewProps) => { - const { inventory } = props; + const { inventory } = props; - return ( - - - {inventory.map(item => )} - - - ); + return ( + + + {inventory.map(item => ( + + ))} + + + ); }; const ReviewCard = (props: ReviewCardProps) => { - const { item } = props; - const { - item_name, description, totalAvailable, maxRequestQty, - imageUrl, category, price, owner, - approvalRequired, returnRequired, hidden, location - } = item; - return ( - - - - {item_name} - Request up to {maxRequestQty} at a time | {totalAvailable} available, Location: {location}, - Owner: {owner}, Unit - Cost: ${price} - - - {hidden ? : null} - {!approvalRequired ? : null} - {!returnRequired ? : null} - - {description} - - - ); + const { item } = props; + const { + item_name, + description, + totalAvailable, + maxRequestQty, + imageUrl, + category, + price, + owner, + approvalRequired, + returnRequired, + hidden, + location, + } = item; + return ( + + + + {item_name} + + Request up to {maxRequestQty} at a time | {totalAvailable} available, Location: {location} + , Owner: {owner}, Unit Cost: ${price} + + + + {hidden ? ( + + ) : null} + {!approvalRequired ? ( + + ) : null} + {!returnRequired ? ( + + ) : null} + + {description} + + + ); }; export default ReviewSetup; diff --git a/client/src/components/csv/CSVUpload.tsx b/client/src/components/csv/CSVUpload.tsx index 6d2e4ab..3ca8926 100644 --- a/client/src/components/csv/CSVUpload.tsx +++ b/client/src/components/csv/CSVUpload.tsx @@ -1,212 +1,214 @@ import React from "react"; -import {Button, Container, Header, Label} from "semantic-ui-react"; -import {withToastManager} from "react-toast-notifications"; -import {ItemComplete} from "../item/ItemEditForm"; -import {unformat} from "accounting"; +import { Button, Container, Header, Label } from "semantic-ui-react"; +import { withToastManager } from "react-toast-notifications"; +import { unformat } from "accounting"; + +import { ItemComplete } from "../item/ItemEditForm"; const templateHeader = [ - "Name", "Description", "Quantity in stock", - "Quantity allowed per request", "Image link", - "Category", "Item value", "Owner", "Return required", - "Approval required", "Hidden", "Serial numbers (comma-separated)" + "Name", + "Description", + "Quantity in stock", + "Quantity allowed per request", + "Image link", + "Category", + "Item value", + "Owner", + "Return required", + "Approval required", + "Hidden", + "Serial numbers (comma-separated)", ]; // Unused, recording for posterity -const typeString = (field: string | null) => field ? field.trim() : ""; +const typeString = (field: string | null) => (field ? field.trim() : ""); const typeNumber = (field: string) => { - const val = parseFloat(field); - return isNaN(val) ? 0 : val; + const val = parseFloat(field); + return isNaN(val) ? 0 : val; }; const typeMoney = (field: string) => unformat(field); const typeBool = (field: string) => field === "1"; // Default indices - kw are uniquely identifiable partial phrases -const fieldInfo: {[field: string]: {index: number, typer: (field: string) => any, kw: string[]}} = { - item_name: {index: 0, typer: typeString, kw: ["name"]}, - description: {index: 1, typer: typeString, kw: ["desc"]}, - totalAvailable: {index: 2, typer: typeNumber, kw: ["total"]}, - maxRequestQty: {index: 3, typer: typeNumber, kw: ["max"]}, - imageUrl: {index: 4, typer: typeString, kw: ["image", "img", "url"]}, - category: {index: 5, typer: typeString, kw: ["category"]}, - price: {index: 6, typer: typeMoney, kw: ["price", "cost", "value"]}, - owner: {index: 7, typer: typeString, kw: ["owner", "who"]}, - returnRequired: {index: 8, typer: typeBool, kw: ["ret"]}, - approvalRequired: {index: 9, typer: typeBool, kw: ["approv"]}, - hidden: {index: 10, typer: typeBool, kw: ["hid"]}, - location: {index: 11, typer: typeString, kw: ["location"]} - // "serial": [11, typeString] // Not implemented +const fieldInfo: { + [field: string]: { index: number; typer: (field: string) => any; kw: string[] }; +} = { + item_name: { index: 0, typer: typeString, kw: ["name"] }, + description: { index: 1, typer: typeString, kw: ["desc"] }, + totalAvailable: { index: 2, typer: typeNumber, kw: ["total"] }, + maxRequestQty: { index: 3, typer: typeNumber, kw: ["max"] }, + imageUrl: { index: 4, typer: typeString, kw: ["image", "img", "url"] }, + category: { index: 5, typer: typeString, kw: ["category"] }, + price: { index: 6, typer: typeMoney, kw: ["price", "cost", "value"] }, + owner: { index: 7, typer: typeString, kw: ["owner", "who"] }, + returnRequired: { index: 8, typer: typeBool, kw: ["ret"] }, + approvalRequired: { index: 9, typer: typeBool, kw: ["approv"] }, + hidden: { index: 10, typer: typeBool, kw: ["hid"] }, + location: { index: 11, typer: typeString, kw: ["location"] }, + // "serial": [11, typeString] // Not implemented }; -const kwToName: {[kw: string]: string} = {}; -Object.keys(fieldInfo).forEach((field) => { - const info = fieldInfo[field]; - info.kw.forEach(key => { - kwToName[key] = field; - }); +const kwToName: { [kw: string]: string } = {}; +Object.keys(fieldInfo).forEach(field => { + const info = fieldInfo[field]; + info.kw.forEach(key => { + kwToName[key] = field; + }); }); interface UploadProps { - setInventory: (inventory: ItemComplete[]) => any; - toastManager: any; + setInventory: (inventory: ItemComplete[]) => any; + toastManager: any; } interface UploadState { - logs: string[]; + logs: string[]; } class UploadStep extends React.Component { - constructor(props: UploadProps) { - super(props); - this.state = { - logs: ["Waiting for upload"] - }; - } - - public addLog = (msg: string) => { - const { logs } = this.state; - logs.push(msg); - this.setState({ logs }); + constructor(props: UploadProps) { + super(props); + this.state = { + logs: ["Waiting for upload"], + }; + } + + public addLog = (msg: string) => { + const { logs } = this.state; + logs.push(msg); + this.setState({ logs }); + }; + + public onCSVSelect = (e: any) => { + if (!e.target.files || !e.target.files[0]) { + return; } - - public onCSVSelect = (e: any) => { - if (!e.target.files || !e.target.files[0]) { - return; + const file = e.target.files[0]; + const reader = new FileReader(); + + const { toastManager, setInventory } = this.props; + + reader.addEventListener("load", (e: any) => { + const csvdata: string = e.target.result; + const lines = csvdata.split("\n"); + // Only request the first line to cleanse headers + const header = lines[0].split("\t").map(field => field.toLowerCase()); + + // Hardcoded header assumption + if (header.length !== templateHeader.length) { + toastManager.add("Improper CSV formatting, header too long", { + appearance: "error", + autoDismiss: true, + placement: "top-center", + }); + return; + } + + // Mutate non-default indices based on headers here + header.forEach((heading, i) => { + Object.keys(kwToName).some(kw => { + if (!heading.includes(kw)) { + return false; + } + fieldInfo[kwToName[kw]].index = i; + return true; + }); + }); + + const items: ItemComplete[] = []; + this.addLog("Starting CSV parse"); + for (let i = 1; i < lines.length; i++) { + const fields = lines[i].split("\t"); + // hack: If we encounter an empty name, end parsing immediately + if (fields[0] === "") { + break; } - const file = e.target.files[0]; - const reader = new FileReader(); - - const { toastManager, setInventory } = this.props; - - reader.addEventListener("load", (e: any) => { - - const csvdata: string = e.target.result; - const lines = csvdata.split("\n"); - // Only request the first line to cleanse headers - const header = lines[0].split("\t").map(field => field.toLowerCase()); - - // Hardcoded header assumption - if (header.length !== templateHeader.length) { - toastManager.add("Improper CSV formatting, header too long", { - appearance: "error", - autoDismiss: true, - placement: "top-center" - }); - return; - } - - // Mutate non-default indices based on headers here - header.forEach((heading, i) => { - Object.keys(kwToName).some(kw => { - if (!heading.includes(kw)) { - return false; - } - fieldInfo[kwToName[kw]].index = i; - return true; - }); - }); - - const items: ItemComplete[] = []; - this.addLog("Starting CSV parse"); - for (let i = 1; i < lines.length; i++) { - const fields = lines[i].split("\t"); - // hack: If we encounter an empty name, end parsing immediately - if (fields[0] === "") { - break; - } - - // TODO: Verify serial number length matches quantity - // Merge all overflow items into serial numbers - // const serialNumbers = fields.slice(11); - - // const quantity = fields[fieldInfo["totalAvailable"].index]; - // if (quantity != serialNumbers.length) { - // this.addLog(`Error: Line ${i+1} malformed.`); - // this.addLog(lines[i]); - // } - - const newItem: { [field: string]: any } = {}; - // For each field, find the appropriate column index, take that field, type, and store - Object.keys(fieldInfo).forEach((key: string) => { - const { index, typer } = fieldInfo[key]; - const rawField: string = fields[index]; - newItem[key] = typer(rawField); - }); - this.addLog(`${newItem.item_name} added: Quantity ${newItem.totalAvailable}`); - items.push(newItem as ItemComplete); - } - setInventory(items); + + // TODO: Verify serial number length matches quantity + // Merge all overflow items into serial numbers + // const serialNumbers = fields.slice(11); + + // const quantity = fields[fieldInfo["totalAvailable"].index]; + // if (quantity != serialNumbers.length) { + // this.addLog(`Error: Line ${i+1} malformed.`); + // this.addLog(lines[i]); + // } + + const newItem: { [field: string]: any } = {}; + // For each field, find the appropriate column index, take that field, type, and store + Object.keys(fieldInfo).forEach((key: string) => { + const { index, typer } = fieldInfo[key]; + const rawField: string = fields[index]; + newItem[key] = typer(rawField); }); - - reader.readAsBinaryString(file); - } + this.addLog(`${newItem.item_name} added: Quantity ${newItem.totalAvailable}`); + items.push(newItem as ItemComplete); + } + setInventory(items); + }); - public render() { - const { logs } = this.state; - return ( + reader.readAsBinaryString(file); + }; + + public render() { + const { logs } = this.state; + return ( + +
+ Upload a TSV +
+ +
+
+
-
- Upload a TSV -
- -
-
-
- -
- Feed -
- {logs.map((log, i) => (

{log}

))} -
-
-
+
Feed
+ {logs.map((log, i) => ( +

{log}

+ ))}
- ); - } +
+
+
+ ); + } } const styles = { - buttonWrapper: { - flex: "none", - width: "15em" - }, - logWrapper: { - borderLeft: "2px solid black", - marginLeft: "1em", - paddingLeft: "1em" - }, - notice: { - marginTop: "1em" - }, - wrapper: { - display: "flex", - } + buttonWrapper: { + flex: "none", + width: "15em", + }, + logWrapper: { + borderLeft: "2px solid black", + marginLeft: "1em", + paddingLeft: "1em", + }, + notice: { + marginTop: "1em", + }, + wrapper: { + display: "flex", + }, }; export default withToastManager(UploadStep); diff --git a/client/src/components/csv/CSVWizard.tsx b/client/src/components/csv/CSVWizard.tsx index 3d9dca4..8a1d7d2 100644 --- a/client/src/components/csv/CSVWizard.tsx +++ b/client/src/components/csv/CSVWizard.tsx @@ -1,207 +1,202 @@ import React from "react"; -import {Button, Container, Dimmer, Header, Loader, Segment, Step} from "semantic-ui-react"; +import { Button, Container, Dimmer, Header, Loader, Segment, Step } from "semantic-ui-react"; +import { withToastManager } from "react-toast-notifications"; +import { Redirect } from "react-router-dom"; +import { Mutation } from "@apollo/client/react/components"; + import UploadStep from "./CSVUpload"; import ReviewStep from "./CSVReview"; -import {ItemComplete} from "../item/ItemEditForm"; -import {withToastManager} from "react-toast-notifications"; -import {Redirect} from "react-router-dom"; -import {Mutation} from "@apollo/client/react/components"; -import {CREATE_ITEM} from "../util/graphql/Mutations"; +import { ItemComplete } from "../item/ItemEditForm"; +import { CREATE_ITEM } from "../util/graphql/Mutations"; interface CSVWizardProps { - toastManager: any; + toastManager: any; } interface CSVWizardState { - wizardStep: number; - inventory: ItemComplete[]; - isStepComplete: boolean; - isSubmitting: boolean; - isComplete: boolean; - itemsCreated: number; - totalItems: number; + wizardStep: number; + inventory: ItemComplete[]; + isStepComplete: boolean; + isSubmitting: boolean; + isComplete: boolean; + itemsCreated: number; + totalItems: number; } interface StepInterface { - key: string; - icon: string; - title: string; - description?: string; - stepDiv?: React.ReactElement; - active?: boolean; - disabled?: boolean; - - [key: string]: any; + key: string; + icon: string; + title: string; + description?: string; + stepDiv?: React.ReactElement; + active?: boolean; + disabled?: boolean; + + [key: string]: any; } class CSVWizard extends React.Component { - constructor(props: any) { - super(props); - this.state = { - wizardStep: 0, - inventory: [], - isStepComplete: false, - isSubmitting: false, - isComplete: false, - itemsCreated: 0, - totalItems: 0 - }; - } - - public nextStep = () => { - const { wizardStep } = this.state; - this.setState({ - wizardStep: wizardStep + 1, - isStepComplete: false }); - } - - public setStepFactory = (newStep: number) => { // Careful, make sure isStepComplete set intentionally - return () => { - if (newStep === 0) { - this.setState({wizardStep: newStep}); - } - }; + constructor(props: any) { + super(props); + this.state = { + wizardStep: 0, + inventory: [], + isStepComplete: false, + isSubmitting: false, + isComplete: false, + itemsCreated: 0, + totalItems: 0, + }; + } + + public nextStep = () => { + const { wizardStep } = this.state; + this.setState({ + wizardStep: wizardStep + 1, + isStepComplete: false, + }); + }; + + public setStepFactory = ( + newStep: number // Careful, make sure isStepComplete set intentionally + ) => () => { + if (newStep === 0) { + this.setState({ wizardStep: newStep }); } - - public setInventory = (inventory: ItemComplete[]) => { - this.setState({inventory, isStepComplete: true}); - } - - public uploadInventory = (createItem: any) => { - const totalItems = this.state.inventory.length; - this.setState({ - isSubmitting: true, - totalItems - }); - - Promise.all(this.state.inventory.map((item) => { - - return createItem({ - variables: { - newItem: item - } - }).then(() => { - const numSubmitted = this.state.itemsCreated; - this.setState({ - itemsCreated: numSubmitted + 1 - }); - }).catch((err: Error) => { - console.error(err); - }); - })).then(() => this.onInventorySubmitted()) - .catch((error) => { - const {toastManager} = this.props; - this.setState({isComplete: true, isSubmitting: false}); - toastManager.add(`Error submitted one or more items: ${error.message}`, { - appearance: "error", - autoDismiss: false, - placement: "top-center" - }); + }; + + public setInventory = (inventory: ItemComplete[]) => { + this.setState({ inventory, isStepComplete: true }); + }; + + public uploadInventory = (createItem: any) => { + const totalItems = this.state.inventory.length; + this.setState({ + isSubmitting: true, + totalItems, + }); + + Promise.all( + this.state.inventory.map(item => + createItem({ + variables: { + newItem: item, + }, + }) + .then(() => { + const numSubmitted = this.state.itemsCreated; + this.setState({ + itemsCreated: numSubmitted + 1, }); - - - } - - public onInventorySubmitted = () => { + }) + .catch((err: Error) => { + console.error(err); + }) + ) + ) + .then(() => this.onInventorySubmitted()) + .catch(error => { const { toastManager } = this.props; - this.setState({isComplete: true, isSubmitting: false}); - toastManager.add("Import successful! You may need to refresh the page to see the new items", { - appearance: "success", - autoDismiss: true, - placement: "top-center" + this.setState({ isComplete: true, isSubmitting: false }); + toastManager.add(`Error submitted one or more items: ${error.message}`, { + appearance: "error", + autoDismiss: false, + placement: "top-center", }); + }); + }; + + public onInventorySubmitted = () => { + const { toastManager } = this.props; + this.setState({ isComplete: true, isSubmitting: false }); + toastManager.add("Import successful! You may need to refresh the page to see the new items", { + appearance: "success", + autoDismiss: true, + placement: "top-center", + }); + }; + + public render() { + const { wizardStep, inventory, isStepComplete, isSubmitting, isComplete } = this.state; + + if (isComplete) { + return ; } - public render() { - const { wizardStep, inventory, isStepComplete, isSubmitting, isComplete } = this.state; - - if (isComplete) { - return ; - } - - const nextButton = ( - - ); - - const submitButton = ( - - { - (createItem: any, {loading, error, data}: any) => ( - - ) - } - - - ); - - const defaultStep = ( - -
- That's weird. Refresh? -
-
- ); - - const stepsSrc: StepInterface[] = [ - { - key: "upload", - icon: "upload", - onClick: this.setStepFactory(0), - title: "Upload", - stepDiv: - }, - { - key: "confirm", - icon: "pencil", - title: "Review Upload", - stepDiv: - }, - ]; - - // Select appropriate step div to show - const activeStepContent = stepsSrc[wizardStep].stepDiv || defaultStep; - - // Set appropriate state in step component and strip stepDiv for proper props - const steps: StepInterface[] = stepsSrc.map((step, i) => { - if (i === wizardStep) { - step.active = true; - } - if (i > wizardStep) { - step.disabled = true; - } - delete step.stepDiv; - return step; - }); - - return ( -
- - - - -
Import Items
- - {wizardStep === stepsSrc.length - 1 ? submitButton : nextButton} -
- - {activeStepContent} - -
- ); - } + const nextButton = ( + + ); + + const submitButton = ( + + {(createItem: any, { loading, error, data }: any) => ( + + )} + + ); + + const defaultStep = ( + +
That's weird. Refresh?
+
+ ); + + const stepsSrc: StepInterface[] = [ + { + key: "upload", + icon: "upload", + onClick: this.setStepFactory(0), + title: "Upload", + stepDiv: , + }, + { + key: "confirm", + icon: "pencil", + title: "Review Upload", + stepDiv: , + }, + ]; + + // Select appropriate step div to show + const activeStepContent = stepsSrc[wizardStep].stepDiv || defaultStep; + + // Set appropriate state in step component and strip stepDiv for proper props + const steps: StepInterface[] = stepsSrc.map((step, i) => { + if (i === wizardStep) { + step.active = true; + } + if (i > wizardStep) { + step.disabled = true; + } + delete step.stepDiv; + return step; + }); + + return ( +
+ + + + +
Import Items
+ + {wizardStep === stepsSrc.length - 1 ? submitButton : nextButton} +
+ {activeStepContent} +
+ ); + } } export default withToastManager(CSVWizard); diff --git a/client/src/components/desk/CardList.tsx b/client/src/components/desk/CardList.tsx index 11b1a80..d487efe 100644 --- a/client/src/components/desk/CardList.tsx +++ b/client/src/components/desk/CardList.tsx @@ -1,57 +1,60 @@ -import React, {ReactElement, useState} from "react"; -import {Container, Grid, Header, Input, Segment} from "semantic-ui-react"; +import React, { ReactElement, useState } from "react"; +import { Container, Grid, Header, Input, Segment } from "semantic-ui-react"; interface CardListProps { - title: string; - length: number; - loading?: boolean; - cards: any; - render: (elem: any) => ReactElement; - filter: (elem: any, query: string) => boolean - empty: any; + title: string; + length: number; + loading?: boolean; + cards: any; + render: (elem: any) => ReactElement; + filter: (elem: any, query: string) => boolean; + empty: any; } const CardList: React.FunctionComponent = props => { - const [searchQuery, setSearchQuery] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); - const cards = props.cards.filter((elem: any) => props.filter(elem, searchQuery)).map((elem: any) => props.render(elem)); - const noResults = ( - -
- No search results found -
-
-
); + const cards = props.cards + .filter((elem: any) => props.filter(elem, searchQuery)) + .map((elem: any) => props.render(elem)); + const noResults = ( + + +
No search results found
+
+
+ ); - let content = cards; + let content = cards; - if (searchQuery && !cards.length) { - content = noResults; - } else if (!cards.length) { - content = props.empty; - } + if (searchQuery && !cards.length) { + content = noResults; + } else if (!cards.length) { + content = props.empty; + } - return ( - -

{props.title}

- { - setSearchQuery(value.trim().toLowerCase()); - }} - /> - {props.length} request{props.length === 1 ? "" : "s"} - - {content} - -
- ); + return ( + +

{props.title}

+ { + setSearchQuery(value.trim().toLowerCase()); + }} + /> + + {props.length} request{props.length === 1 ? "" : "s"} + + {content} +
+ ); }; export default CardList; diff --git a/client/src/components/desk/DeskContainer.tsx b/client/src/components/desk/DeskContainer.tsx index 8b87ba1..dc78527 100644 --- a/client/src/components/desk/DeskContainer.tsx +++ b/client/src/components/desk/DeskContainer.tsx @@ -1,192 +1,210 @@ -import React, {useState} from "react"; -import {connect} from "react-redux"; -import {Checkbox, DropdownProps, Grid, Header, Loader, Message, Select} from "semantic-ui-react"; +import React, { useState } from "react"; +import { connect } from "react-redux"; +import { Checkbox, DropdownProps, Grid, Header, Loader, Message, Select } from "semantic-ui-react"; +import { useQuery } from "@apollo/client"; + import SubmittedList from "./submitted/SubmittedList"; -import {useQuery} from "@apollo/client"; -import {REQUEST_CHANGE} from "../util/graphql/Subscriptions"; +import { REQUEST_CHANGE } from "../util/graphql/Subscriptions"; import ReadyToPrepareList from "./fulfillment/ReadyToPrepareList"; -import {DESK_REQUESTS} from "../util/graphql/Queries"; -import {Request, RequestStatus} from "../../types/Request"; -import {APPROVED, DAMAGED, FULFILLED, Location, LOST, READY_FOR_PICKUP, SUBMITTED} from "../../types/Hardware"; -import {pickRandomElement} from "../admin/AdminOverviewContainer"; +import { DESK_REQUESTS } from "../util/graphql/Queries"; +import { Request, RequestStatus } from "../../types/Request"; +import { + APPROVED, + DAMAGED, + FULFILLED, + Location, + LOST, + READY_FOR_PICKUP, + SUBMITTED, +} from "../../types/Hardware"; +import { pickRandomElement } from "../admin/AdminOverviewContainer"; import ReadyForPickupList from "./pickup/ReadyForPickupList"; import ReadyForReturnList from "./returns/ReadyForReturnList"; function mapStateToProps(state: any) { - return {}; + return {}; } -function getRequestsWithStatus(requests: Request[], statuses: RequestStatus[], location_id: number = 0) { - - return requests.filter((r: Request) => { - return (location_id === 0 || r.item.location.location_id === location_id) - && statuses.some(status => r.status === status); - }); +function getRequestsWithStatus(requests: Request[], statuses: RequestStatus[], location_id = 0) { + return requests.filter( + (r: Request) => + (location_id === 0 || r.item.location.location_id === location_id) && + statuses.some(status => r.status === status) + ); } -function getConsolidatedRequestsWithStatus(requests: Request[], statuses: RequestStatus[], location_id: number = 0) { - const filteredRequests = getRequestsWithStatus(requests, statuses, location_id); - const requestsByUser: any = {}; - - for (let i = 0; i < filteredRequests.length; i++) { - const req = filteredRequests[i]; - if (!requestsByUser.hasOwnProperty(req.user.uuid)) { - requestsByUser[req.user.uuid] = { - user: req.user, - requests: [] - }; - } - - requestsByUser[req.user.uuid].requests.push(req); +function getConsolidatedRequestsWithStatus( + requests: Request[], + statuses: RequestStatus[], + location_id = 0 +) { + const filteredRequests = getRequestsWithStatus(requests, statuses, location_id); + const requestsByUser: any = {}; + + for (let i = 0; i < filteredRequests.length; i++) { + const req = filteredRequests[i]; + if (!requestsByUser.hasOwnProperty(req.user.uuid)) { + requestsByUser[req.user.uuid] = { + user: req.user, + requests: [], + }; } - return Object.values(requestsByUser); + + requestsByUser[req.user.uuid].requests.push(req); + } + return Object.values(requestsByUser); } const funPhrases = [ - "Arduino Unos", - "mangoes", - "Raspberry Pis", - "HackGT check-in boxes", - "copies of Bolt", - "screws", - "Oculus Rifts", - "LED strings", - "VR headsets", - "3D printers" + "Arduino Unos", + "mangoes", + "Raspberry Pis", + "HackGT check-in boxes", + "copies of Bolt", + "screws", + "Oculus Rifts", + "LED strings", + "VR headsets", + "3D printers", ]; -const starters = [ - "I am requesting", - "I would like", - "Please give me" -]; +const starters = ["I am requesting", "I would like", "Please give me"]; const endings = [ - "if you have any", - "as soon as possible", - "within the hour", - `in ${Math.floor((Math.random() + 1) * 60)} minutes` + "if you have any", + "as soon as possible", + "within the hour", + `in ${Math.floor((Math.random() + 1) * 60)} minutes`, ]; - function getUpdateQuery() { - return (prev: any, {subscriptionData}: any) => { - if (!subscriptionData.data) { - return prev; - } - const updatedRequest = subscriptionData.data; - const index = prev.requests.findIndex((x: any) => { - return x.request_id === updatedRequest.request_change.request_id; - }); - - const requests = prev.requests; - - if (index === -1) { // request wasn't returned with original query; add it to array of requests - // If adding a new request to the array of requests, you have to do it this way rather than just pushing to the array - return Object.assign({}, prev, { - requests: [updatedRequest.request_change, ...prev.requests] - }); - } else { // request was returned with original query; update it - requests[index] = updatedRequest.request_change; - } - return {requests}; - }; -} + return (prev: any, { subscriptionData }: any) => { + if (!subscriptionData.data) { + return prev; + } + const updatedRequest = subscriptionData.data; + const index = prev.requests.findIndex( + (x: any) => x.request_id === updatedRequest.request_change.request_id + ); -function DeskContainer() { - const {subscribeToMore, ...query} = useQuery(DESK_REQUESTS); - const randomPhrase = useState(`${pickRandomElement(starters)} ${Math.floor((Math.random() + 1) * 900)} ${pickRandomElement(funPhrases)} ${pickRandomElement(endings)}`); - const [returnsMode, setReturnsMode] = useState(false); - const [location, setLocation] = useState(); + const { requests } = prev; - if (query.loading) { - return ; - } - if (query.error) { - return ; - } + if (index === -1) { + // request wasn't returned with original query; add it to array of requests + // If adding a new request to the array of requests, you have to do it this way rather than just pushing to the array + return { ...prev, requests: [updatedRequest.request_change, ...prev.requests] }; + } // request was returned with original query; update it + requests[index] = updatedRequest.request_change; - if (!location) { - return <> -
- Hardware Desk - {randomPhrase} -
-
Select a location to continue
- { - return { - key: location.location_id, - value: location.location_id, - text: location.location_name - }; - }) - } - onChange={(event: React.SyntheticEvent, data: DropdownProps): void => { - const value: any = data.value; - setLocation(value); - }} - /> - - - - - - + ); + } + + if (!location) { + return ( + <> +
+ Hardware Desk + {randomPhrase} +
+
Select a location to continue
+ ({ + key: location.location_id, + value: location.location_id, + text: location.location_name, + }))} + onChange={(event: React.SyntheticEvent, data: DropdownProps): void => { + const { value } = data; + setLocation(value); + }} + /> + + + + + + + ); } export default connect(mapStateToProps)(DeskContainer); diff --git a/client/src/components/desk/DeskUtil.tsx b/client/src/components/desk/DeskUtil.tsx index 4265888..ad533bf 100644 --- a/client/src/components/desk/DeskUtil.tsx +++ b/client/src/components/desk/DeskUtil.tsx @@ -1,24 +1,31 @@ -import {Request} from "../../types/Request"; +import { Request } from "../../types/Request"; -export function updateRequestStatus(updateRequest: any, request_id: number, newStatus: any, updatedHaveID: boolean | null = null) { - const updatedRequest: any = { - request_id, - new_status: newStatus - }; +export function updateRequestStatus( + updateRequest: any, + request_id: number, + newStatus: any, + updatedHaveID: boolean | null = null +) { + const updatedRequest: any = { + request_id, + new_status: newStatus, + }; - if (updatedHaveID !== null) { - updatedRequest.user_haveID = updatedHaveID; - } - return updateRequest({ - variables: { - updatedRequest - } - }); + if (updatedHaveID !== null) { + updatedRequest.user_haveID = updatedHaveID; + } + return updateRequest({ + variables: { + updatedRequest, + }, + }); } export function requestSearch(r: Request, searchQuery: string): boolean { - return r.user.name.toLowerCase().includes(searchQuery) - || r.user.slackUsername.toLowerCase().includes(searchQuery) - || r.user.email.toLowerCase().includes(searchQuery) - || r.user.phone.includes(searchQuery); + return ( + r.user.name.toLowerCase().includes(searchQuery) || + r.user.slackUsername.toLowerCase().includes(searchQuery) || + r.user.email.toLowerCase().includes(searchQuery) || + r.user.phone.includes(searchQuery) + ); } diff --git a/client/src/components/desk/ItemAndQuantity.tsx b/client/src/components/desk/ItemAndQuantity.tsx index d97694b..7beb358 100644 --- a/client/src/components/desk/ItemAndQuantity.tsx +++ b/client/src/components/desk/ItemAndQuantity.tsx @@ -1,18 +1,20 @@ -import React from 'react'; -import {Label} from "semantic-ui-react"; +import React from "react"; +import { Label } from "semantic-ui-react"; interface Props { - quantity: number; - itemName: string; + quantity: number; + itemName: string; } -function ItemAndQuantity({quantity, itemName, ...props}: Props) { - return <> - - {itemName} - ; +function ItemAndQuantity({ quantity, itemName, ...props }: Props) { + return ( + <> + + {itemName} + + ); } export default ItemAndQuantity; diff --git a/client/src/components/desk/fulfillment/ReadyToPrepareCard.tsx b/client/src/components/desk/fulfillment/ReadyToPrepareCard.tsx index 2275021..42b6235 100644 --- a/client/src/components/desk/fulfillment/ReadyToPrepareCard.tsx +++ b/client/src/components/desk/fulfillment/ReadyToPrepareCard.tsx @@ -1,94 +1,137 @@ import React from "react"; -import {Button, Card, Header, Icon, Popup} from "semantic-ui-react"; +import { Button, Card, Header, Icon, Popup } from "semantic-ui-react"; import TimeAgo from "react-timeago"; -import {Request, UserAndRequests} from "../../../types/Request"; -import ItemAndQuantity from "../ItemAndQuantity"; -import {READY_FOR_PICKUP, SUBMITTED} from "../../../types/Hardware"; -import {useMutation} from "@apollo/client"; -import {UPDATE_REQUEST} from "../../util/graphql/Mutations"; -import {updateRequestStatus} from "../DeskUtil"; +import { useMutation } from "@apollo/client"; +import { Request, UserAndRequests } from "../../../types/Request"; +import ItemAndQuantity from "../ItemAndQuantity"; +import { READY_FOR_PICKUP, SUBMITTED } from "../../../types/Hardware"; +import { UPDATE_REQUEST } from "../../util/graphql/Mutations"; +import { updateRequestStatus } from "../DeskUtil"; interface ReadyToFulfillCardProps { - card: UserAndRequests + card: UserAndRequests; } -function ReadyToPrepareCard({card}: ReadyToFulfillCardProps) { - const [updateRequest, {loading, error}] = useMutation(UPDATE_REQUEST); - - // @ts-ignore - card.requests.sort((a: Request, b: Request) => new Date(a.updatedAt) - new Date(b.updatedAt)); +function ReadyToPrepareCard({ card }: ReadyToFulfillCardProps) { + const [updateRequest, { loading, error }] = useMutation(UPDATE_REQUEST); - return ( - - -
- {card.user.name} -
-
- { - card.requests.map(request => - -   - #{request.request_id} + // @ts-ignore + card.requests.sort((a: Request, b: Request) => new Date(a.updatedAt) - new Date(b.updatedAt)); -
- updateRequestStatus(updateRequest, request.request_id, SUBMITTED)}> - - } - content="Return to Submitted" - /> - updateRequestStatus(updateRequest, request.request_id, READY_FOR_PICKUP)}> - - } - content="Mark Ready for Pickup" - /> -
-
) - } + return ( + + +
{card.user.name}
+
+ {card.requests.map(request => ( + + + + +   + #{request.request_id} +
+ + updateRequestStatus(updateRequest, request.request_id, SUBMITTED) + } + > + + + } + content="Return to Submitted" + /> + + updateRequestStatus(updateRequest, request.request_id, READY_FOR_PICKUP) + } + > + + + } + content="Mark Ready for Pickup" + /> +
+
+ ))} - - - - - {card.user.slackUsername} - - {error ? - Unable to change request status: {error.message} - : ""} - -
- - - card.requests.forEach(request => - updateRequestStatus(updateRequest, request.request_id, SUBMITTED) - ) - }> - - } - content="Return all to Submitted" - /> - - card.requests.forEach(request => - updateRequestStatus(updateRequest, request.request_id, READY_FOR_PICKUP) - ) - }> - - RFP - } - content="Mark all Ready for Pickup" - /> - -
-
-
- ); + + + + + + {card.user.slackUsername} + + {error ? ( + + + Unable to change request status: {error.message} + + ) : ( + "" + )} + +
+ + + card.requests.forEach(request => + updateRequestStatus(updateRequest, request.request_id, SUBMITTED) + ) + } + > + + + } + content="Return all to Submitted" + /> + + card.requests.forEach(request => + updateRequestStatus(updateRequest, request.request_id, READY_FOR_PICKUP) + ) + } + > + + RFP + + } + content="Mark all Ready for Pickup" + /> + +
+
+
+ ); } export default ReadyToPrepareCard; diff --git a/client/src/components/desk/fulfillment/ReadyToPrepareList.tsx b/client/src/components/desk/fulfillment/ReadyToPrepareList.tsx index 1187673..3dbd292 100644 --- a/client/src/components/desk/fulfillment/ReadyToPrepareList.tsx +++ b/client/src/components/desk/fulfillment/ReadyToPrepareList.tsx @@ -1,27 +1,29 @@ -import React from 'react'; -import {Container, Header, Segment} from "semantic-ui-react"; +import React from "react"; +import { Container, Header, Segment } from "semantic-ui-react"; + import CardList from "../CardList"; import ReadyToPrepareCard from "./ReadyToPrepareCard"; -import {requestSearch} from "../DeskUtil"; +import { requestSearch } from "../DeskUtil"; -function ReadyToPrepareList({cards}: any) { - const empty = - -
- Nothing to prepare. Take a break! -
-
-
; +function ReadyToPrepareList({ cards }: any) { + const empty = ( + + +
Nothing to prepare. Take a break!
+
+
+ ); - return ( - } - empty={empty} - /> - ); + return ( + } + empty={empty} + /> + ); } export default ReadyToPrepareList; diff --git a/client/src/components/desk/pickup/PhotoIdCheck.tsx b/client/src/components/desk/pickup/PhotoIdCheck.tsx index 372c632..269b3bf 100644 --- a/client/src/components/desk/pickup/PhotoIdCheck.tsx +++ b/client/src/components/desk/pickup/PhotoIdCheck.tsx @@ -1,74 +1,126 @@ -import React from 'react'; -import {Button, Card, Icon} from "semantic-ui-react"; -import {updateRequestStatus} from "../DeskUtil"; -import {FULFILLED} from "../../../types/Hardware"; +import React from "react"; +import { Button, Card, Icon } from "semantic-ui-react"; -function generateCard({title, content, button, error, highlightTitle}: any) { - return ( - - {title} - - - {content} +import { updateRequestStatus } from "../DeskUtil"; +import { FULFILLED } from "../../../types/Hardware"; + +function generateCard({ title, content, button, error, highlightTitle }: any) { + return ( + + + {title} + + {content} + {error ? ( + + + Unable to change request status: {error.message} - {error ? - Unable to change request status: {error.message} - : ""} - {button} - ); + ) : ( + "" + )} + {button} + + ); } -function PhotoIdCheck({userName, loading, updateRequest, requests, returnRequired, haveID, error}: any) { - const idRequired = { - title: <>Photo ID required, - content: `Did you collect ${userName}'s photo ID?`, - button: ( - - - ), - error, - highlightTitle: true - }; +function PhotoIdCheck({ + userName, + loading, + updateRequest, + requests, + returnRequired, + haveID, + error, +}: any) { + const idRequired = { + title: ( + <> + + Photo ID required + + ), + content: `Did you collect ${userName}'s photo ID?`, + button: ( + + + + + ), + error, + highlightTitle: true, + }; - const noIdRequiredButton = ; + } + > + Complete pickup + + ); - const idAlreadyCollected = { - title: All set!, - content: `You already have ${userName}'s photo ID`, - button: noIdRequiredButton, - error, - highlightTitle: false - }; + const idAlreadyCollected = { + title: ( + + + All set! + + ), + content: `You already have ${userName}'s photo ID`, + button: noIdRequiredButton, + error, + highlightTitle: false, + }; - const idNotRequired = { - title: All set!, - content: `${requests.length === 1 ? "This item doesn't" : "None of these items"} require return, so no photo ID is required for pickup`, - button: noIdRequiredButton, - error, - highlightTitle: false - }; + const idNotRequired = { + title: ( + + + All set! + + ), + content: `${ + requests.length === 1 ? "This item doesn't" : "None of these items" + } require return, so no photo ID is required for pickup`, + button: noIdRequiredButton, + error, + highlightTitle: false, + }; - if (!returnRequired) { - return generateCard(idNotRequired); - } + if (!returnRequired) { + return generateCard(idNotRequired); + } - if (returnRequired && !haveID) { - return generateCard(idRequired); - } + if (returnRequired && !haveID) { + return generateCard(idRequired); + } - return generateCard(idAlreadyCollected); // returnRequired && haveID + return generateCard(idAlreadyCollected); // returnRequired && haveID } export default PhotoIdCheck; diff --git a/client/src/components/desk/pickup/ReadyForPickupCard.tsx b/client/src/components/desk/pickup/ReadyForPickupCard.tsx index d9488a1..29961cc 100644 --- a/client/src/components/desk/pickup/ReadyForPickupCard.tsx +++ b/client/src/components/desk/pickup/ReadyForPickupCard.tsx @@ -1,110 +1,152 @@ import React from "react"; -import {Button, Card, Header, Icon, Popup} from "semantic-ui-react"; +import { Button, Card, Header, Icon, Popup } from "semantic-ui-react"; import TimeAgo from "react-timeago"; -import {Request, UserAndRequests} from "../../../types/Request"; +import { useMutation } from "@apollo/client"; + +import { Request, UserAndRequests } from "../../../types/Request"; import ItemAndQuantity from "../ItemAndQuantity"; -import {APPROVED} from "../../../types/Hardware"; -import {useMutation} from "@apollo/client"; -import {UPDATE_REQUEST} from "../../util/graphql/Mutations"; -import {updateRequestStatus} from "../DeskUtil"; +import { APPROVED } from "../../../types/Hardware"; +import { UPDATE_REQUEST } from "../../util/graphql/Mutations"; +import { updateRequestStatus } from "../DeskUtil"; import PhotoIdCheck from "./PhotoIdCheck"; - interface ReadyForPickupCardProps { - card: UserAndRequests + card: UserAndRequests; } function returnRequired(requests: Request[]): boolean { - return requests.some(request => request.item.returnRequired); + return requests.some(request => request.item.returnRequired); } -function ReadyForPickupCard({card}: ReadyForPickupCardProps) { - const [updateRequest, {loading, error}] = useMutation(UPDATE_REQUEST); - - // @ts-ignore - card.requests.sort((a: Request, b: Request) => new Date(a.updatedAt) - new Date(b.updatedAt)); - - return ( - - -
- {card.user.name} -
-
- {!card.user.haveID && returnRequired(card.requests) ? - - Photo ID required - : "" - } - { - card.requests.map(request => - -   - #{request.request_id} +function ReadyForPickupCard({ card }: ReadyForPickupCardProps) { + const [updateRequest, { loading, error }] = useMutation(UPDATE_REQUEST); -
- updateRequestStatus(updateRequest, request.request_id, APPROVED)}> - - } - content="Return to Preparing" - /> - - - } - content={} - /> -
-
) - } + // @ts-ignore + card.requests.sort((a: Request, b: Request) => new Date(a.updatedAt) - new Date(b.updatedAt)); - - - - - {card.user.slackUsername} - - {error ? - Unable to change request status: {error.message} - : ""} - -
- - - card.requests.forEach(request => - updateRequestStatus(updateRequest, request.request_id, APPROVED) - ) - }> - - } - content="Return all to Ready to Prepare" - /> - - - Fulfilled - } - content={} - /> + return ( + + +
{card.user.name}
+
+ {!card.user.haveID && returnRequired(card.requests) ? ( + + Photo ID required + + ) : ( + "" + )} + {card.requests.map(request => ( + + + + +   + #{request.request_id} +
+ + updateRequestStatus(updateRequest, request.request_id, APPROVED) + } + > + + + } + content="Return to Preparing" + /> + + + + } + content={ + + } + /> +
+
+ ))} -
-
-
-
- ); + + + + + + {card.user.slackUsername} + + {error ? ( + + + Unable to change request status: {error.message} + + ) : ( + "" + )} + +
+ + + card.requests.forEach(request => + updateRequestStatus(updateRequest, request.request_id, APPROVED) + ) + } + > + + + } + content="Return all to Ready to Prepare" + /> + + + Fulfilled + + } + content={ + + } + /> + +
+
+ + ); } export default ReadyForPickupCard; diff --git a/client/src/components/desk/pickup/ReadyForPickupList.tsx b/client/src/components/desk/pickup/ReadyForPickupList.tsx index c6b4647..db2e4ad 100644 --- a/client/src/components/desk/pickup/ReadyForPickupList.tsx +++ b/client/src/components/desk/pickup/ReadyForPickupList.tsx @@ -1,26 +1,29 @@ -import React from 'react'; -import {Container, Header, Segment} from "semantic-ui-react"; +import React from "react"; +import { Container, Header, Segment } from "semantic-ui-react"; + import CardList from "../CardList"; import ReadyForPickupCard from "./ReadyForPickupCard"; -import {requestSearch} from "../DeskUtil"; +import { requestSearch } from "../DeskUtil"; -function ReadyForPickupList({cards}: any) { - const empty = ( - -
- No requests awaiting pickup. Take a break! -
-
-
); +function ReadyForPickupList({ cards }: any) { + const empty = ( + + +
No requests awaiting pickup. Take a break!
+
+
+ ); - return ( - } - empty={empty}/> - ); + return ( + } + empty={empty} + /> + ); } export default ReadyForPickupList; diff --git a/client/src/components/desk/returns/PhotoIdReturn.tsx b/client/src/components/desk/returns/PhotoIdReturn.tsx index 98e46ac..b460b6c 100644 --- a/client/src/components/desk/returns/PhotoIdReturn.tsx +++ b/client/src/components/desk/returns/PhotoIdReturn.tsx @@ -1,124 +1,189 @@ -import React, {useState} from 'react'; -import {Button, Card, Dropdown, Icon} from "semantic-ui-react"; -import {updateRequestStatus} from "../DeskUtil"; -import {DAMAGED, LOST, RETURNED} from "../../../types/Hardware"; +import React, { useState } from "react"; +import { Button, Card, Dropdown, Icon } from "semantic-ui-react"; + +import { updateRequestStatus } from "../DeskUtil"; +import { DAMAGED, LOST, RETURNED } from "../../../types/Hardware"; function toDropdownOptions(options: string[]) { - return options.map(option => { - return { - text: `${option.charAt(0).toUpperCase()}${option.slice(1).toLowerCase()}`, - value: option - }; - }); + return options.map(option => ({ + text: `${option.charAt(0).toUpperCase()}${option.slice(1).toLowerCase()}`, + value: option, + })); } -function PhotoIdCheck({userName, loading, updateRequest, requests, returnRequired, haveID, error, setOpen, optional, numReturnRequired}: any) { - const [returnType, setReturnType] = useState(RETURNED); +function PhotoIdCheck({ + userName, + loading, + updateRequest, + requests, + returnRequired, + haveID, + error, + setOpen, + optional, + numReturnRequired, +}: any) { + const [returnType, setReturnType] = useState(RETURNED); - const returnID = { - title: <>Return photo ID, - content: `Did you return ${userName}'s photo ID?`, - button: ( - - - ), - highlightTitle: true - }; + const returnID = { + title: ( + <> + + Return photo ID + + ), + content: `Did you return ${userName}'s photo ID?`, + button: ( + + + + + ), + highlightTitle: true, + }; - const keepIDButton = ; + }} + > + {returnType === RETURNED ? "Complete return" : `Mark ${returnType.toLowerCase()}`} + + ); - let dropdownOptions = [RETURNED, LOST, DAMAGED]; - const alreadyReturnedID = { - title: All set!, - content: `${userName} should already have their photo ID`, - button: keepIDButton, - highlightTitle: false - }; + const dropdownOptions = [RETURNED, LOST, DAMAGED]; + const alreadyReturnedID = { + title: ( + + + All set! + + ), + content: `${userName} should already have their photo ID`, + button: keepIDButton, + highlightTitle: false, + }; - const keepID = { - title: Keep photo ID!, - content: `${userName} has at least one other request that requires return, so keep their photo ID`, - button: keepIDButton, - highlightTitle: false - }; + const keepID = { + title: ( + + + Keep photo ID! + + ), + content: `${userName} has at least one other request that requires return, so keep their photo ID`, + button: keepIDButton, + highlightTitle: false, + }; - let card; + let card; - // Top secret machine learning to decide whether to keep or return photo ID or indicate if we already returned it - if (!returnRequired) { // If this item does not require return - if (!numReturnRequired) { // If this user doesn't have any other items requiring return either - if (haveID) { // If we have their ID - card = returnID; // return it - } else { // We already returned it (or never had it) - card = alreadyReturnedID; - } - } else { // If this user has other items that require return - if (haveID) { // and we have their ID - card = keepID; // keep it - } else { - card = alreadyReturnedID; // we don't have it - } - } - } else { // this item requires return - if (numReturnRequired === 0 || // Technically this can be condensed, but this case is set aside for clarity. - // Specifically, if this item requires return, but this request has been marked lost or damaged, - // then it is possible that even though this item requires return, there are no remaining items that can be returned - // (an example: if you have 2 optional requests and 2 requests marked lost) - // If, in this very very specific case, we still have the user's photo ID, we should return it. - // See also https://github.com/HackGT/bolt/issues/92 - numReturnRequired === 1 || numReturnRequired === requests.length) { // if this is the only item to be returned or every item is being returned - if (haveID) { // and we have their ID - card = returnID; // return it - } else { - card = alreadyReturnedID; // we already gave it back - } - } else { // there are other items that have to be returned - if (haveID) { // and we have their ID - card = keepID; // keep their ID - } else { - card = alreadyReturnedID; // we already returned their ID - } - } + // Top secret machine learning to decide whether to keep or return photo ID or indicate if we already returned it + if (!returnRequired) { + // If this item does not require return + if (!numReturnRequired) { + // If this user doesn't have any other items requiring return either + if (haveID) { + // If we have their ID + card = returnID; // return it + } else { + // We already returned it (or never had it) + card = alreadyReturnedID; + } + } else { + // If this user has other items that require return + if (haveID) { + // and we have their ID + card = keepID; // keep it + } else { + card = alreadyReturnedID; // we don't have it + } + } + } else { + // this item requires return + if ( + numReturnRequired === 0 || // Technically this can be condensed, but this case is set aside for clarity. + // Specifically, if this item requires return, but this request has been marked lost or damaged, + // then it is possible that even though this item requires return, there are no remaining items that can be returned + // (an example: if you have 2 optional requests and 2 requests marked lost) + // If, in this very very specific case, we still have the user's photo ID, we should return it. + // See also https://github.com/HackGT/bolt/issues/92 + numReturnRequired === 1 || + numReturnRequired === requests.length + ) { + // if this is the only item to be returned or every item is being returned + if (haveID) { + // and we have their ID + card = returnID; // return it + } else { + card = alreadyReturnedID; // we already gave it back + } + } else { + // there are other items that have to be returned + if (haveID) { + // and we have their ID + card = keepID; // keep their ID + } else { + card = alreadyReturnedID; // we already returned their ID + } } + } - return ( - - {card.title} - + return ( + + + {card.title} + + {card.content} + {returnRequired && ( - {card.content} + New status:{" "} + setReturnType(value)} + options={toDropdownOptions(dropdownOptions)} + /> - {returnRequired && - New status: setReturnType(value)} - options={toDropdownOptions(dropdownOptions)}/> - } - {error && - Unable to change request status: {error.message} - } - {card.button} - ); - + )} + {error && ( + + + Unable to change request status: {error.message} + + )} + {card.button} + + ); } export default PhotoIdCheck; diff --git a/client/src/components/desk/returns/ReadyForReturnCard.tsx b/client/src/components/desk/returns/ReadyForReturnCard.tsx index b7496e5..4b96b28 100644 --- a/client/src/components/desk/returns/ReadyForReturnCard.tsx +++ b/client/src/components/desk/returns/ReadyForReturnCard.tsx @@ -1,153 +1,180 @@ -import React, {useState} from "react"; -import {Button, Card, Header, Icon, Label, Popup} from "semantic-ui-react"; +import React, { useState } from "react"; +import { Button, Card, Header, Icon, Label, Popup } from "semantic-ui-react"; import TimeAgo from "react-timeago"; -import {Request, UserAndRequests} from "../../../types/Request"; +import { useMutation } from "@apollo/client"; + +import { Request, UserAndRequests } from "../../../types/Request"; import ItemAndQuantity from "../ItemAndQuantity"; -import { - DAMAGED, - FULFILLED, - LOST, - READY_FOR_PICKUP -} from "../../../types/Hardware"; -import {useMutation} from "@apollo/client"; -import {UPDATE_REQUEST} from "../../util/graphql/Mutations"; -import {updateRequestStatus} from "../DeskUtil"; +import { DAMAGED, FULFILLED, LOST, READY_FOR_PICKUP } from "../../../types/Hardware"; +import { UPDATE_REQUEST } from "../../util/graphql/Mutations"; +import { updateRequestStatus } from "../DeskUtil"; import PhotoIdReturn from "./PhotoIdReturn"; - interface ReadyForReturnCardProps { - card: UserAndRequests + card: UserAndRequests; } function returnRequiredFulfilledRequests(requests: Request[]): Request[] { - return requests.filter(request => request.item.returnRequired && request.status === FULFILLED); + return requests.filter(request => request.item.returnRequired && request.status === FULFILLED); } function numReturnRequired(requests: Request[]): number { - return returnRequiredFulfilledRequests(requests).length; + return returnRequiredFulfilledRequests(requests).length; } -function WarningLabel({text}: { text: string }) { - return ; +function WarningLabel({ text }: { text: string }) { + return ( + + ); } function ControlledPopup(props: any) { - let [isOpen, setOpen] = useState(false); + const [isOpen, setOpen] = useState(false); - return setOpen(true)} - onClose={() => setOpen(false)} - trigger={props.children} - content={} - />; + return ( + setOpen(true)} + onClose={() => setOpen(false)} + trigger={props.children} + content={ + + } + /> + ); } -function ReadyForReturnCard({card}: ReadyForReturnCardProps) { - const [updateRequest, {loading, error}] = useMutation(UPDATE_REQUEST); - - // @ts-ignore - card.requests.sort((a: Request, b: Request) => a.item.returnRequired - b.item.returnRequired || a.request_id - b.request_id); - const requiredRequests = returnRequiredFulfilledRequests(card.requests); - return ( - - -
- {card.user.name} -
-
- { - card.requests.map(request => - - -   - #{request.request_id} - -
- {request.status === LOST && } - {request.status === DAMAGED && - } - {request.status === FULFILLED && !request.item.returnRequired && - Optional} - content={`${card.user.name} is not required to return this item`} - />} - -   - - - -
- - {request.item.location.location_name} - -
) - } +function ReadyForReturnCard({ card }: ReadyForReturnCardProps) { + const [updateRequest, { loading, error }] = useMutation(UPDATE_REQUEST); - - - - - {card.user.slackUsername} - - {error ? - Unable to change request - status: {error.message} - : ""} - -
- - - card.requests.forEach(request => - updateRequestStatus(updateRequest, request.request_id, READY_FOR_PICKUP) - ) - }> - - } - content="Return all to Ready for Pickup" - /> - = 1} - numReturnRequired={numReturnRequired(requiredRequests)} - > - {numReturnRequired(requiredRequests) >= 1 && - } - + // @ts-ignore + card.requests.sort( + (a: Request, b: Request) => + a.item.returnRequired - b.item.returnRequired || a.request_id - b.request_id + ); + const requiredRequests = returnRequiredFulfilledRequests(card.requests); + return ( + + +
{card.user.name}
+
+ {card.requests.map(request => ( + + + + +   + #{request.request_id} +
+ {request.status === LOST && } + {request.status === DAMAGED && } + {request.status === FULFILLED && !request.item.returnRequired && ( + + Optional + + } + content={`${card.user.name} is not required to return this item`} + /> + )} +   + + + +
+ + + {request.item.location.location_name} + +
+ ))} -
-
-
-
- ); + + + + + + {card.user.slackUsername} + + {error ? ( + + + Unable to change request status: {error.message} + + ) : ( + "" + )} + +
+ + + card.requests.forEach(request => + updateRequestStatus(updateRequest, request.request_id, READY_FOR_PICKUP) + ) + } + > + + + } + content="Return all to Ready for Pickup" + /> + = 1} + numReturnRequired={numReturnRequired(requiredRequests)} + > + {numReturnRequired(requiredRequests) >= 1 && ( + + )} + + +
+
+ + ); } export default ReadyForReturnCard; diff --git a/client/src/components/desk/returns/ReadyForReturnList.tsx b/client/src/components/desk/returns/ReadyForReturnList.tsx index 3c595dd..21015ea 100644 --- a/client/src/components/desk/returns/ReadyForReturnList.tsx +++ b/client/src/components/desk/returns/ReadyForReturnList.tsx @@ -1,30 +1,31 @@ -import React from 'react'; -import {Container, Header, Segment} from "semantic-ui-react"; +import React from "react"; +import { Container, Header, Segment } from "semantic-ui-react"; + import CardList from "../CardList"; import ReadyForReturnCard from "./ReadyForReturnCard"; -import {requestSearch} from "../DeskUtil"; - -function ReadyForReturnList({cards}: { cards: any }) { - const empty = ( - -
- No requests eligible for return. Take a break! -
-
-
); +import { requestSearch } from "../DeskUtil"; +function ReadyForReturnList({ cards }: { cards: any }) { + const empty = ( + + +
No requests eligible for return. Take a break!
+
+
+ ); - const allCards = cards.sort((a: any, b: any) => a.user.name.localeCompare(b.user.name)); + const allCards = cards.sort((a: any, b: any) => a.user.name.localeCompare(b.user.name)); - return ( - - } - filter={requestSearch} - empty={empty} - /> - ); + return ( + } + filter={requestSearch} + empty={empty} + /> + ); } export default ReadyForReturnList; diff --git a/client/src/components/desk/submitted/SubmittedCard.tsx b/client/src/components/desk/submitted/SubmittedCard.tsx index aae305a..0685127 100644 --- a/client/src/components/desk/submitted/SubmittedCard.tsx +++ b/client/src/components/desk/submitted/SubmittedCard.tsx @@ -1,87 +1,126 @@ import React from "react"; -import {Button, Card, Header, Icon, Label, Popup} from "semantic-ui-react"; +import { Button, Card, Header, Icon, Label, Popup } from "semantic-ui-react"; import TimeAgo from "react-timeago"; -import {Request} from "../../../types/Request"; +import { useMutation } from "@apollo/client"; + +import { Request } from "../../../types/Request"; import ItemAndQuantity from "../ItemAndQuantity"; -import {useMutation} from "@apollo/client"; -import {UPDATE_REQUEST} from "../../util/graphql/Mutations"; -import {APPROVED, DENIED} from "../../../types/Hardware"; +import { UPDATE_REQUEST } from "../../util/graphql/Mutations"; +import { APPROVED, DENIED } from "../../../types/Hardware"; interface SubmittedCardProps { - request: Request; + request: Request; } function noStockWarning(remaining: number) { - return - {`Insufficient stock (${remaining} available for approval)`} - ; + return ( + + + {`Insufficient stock (${remaining} available for approval)`} + + ); } -function SubmittedCard({request}: SubmittedCardProps) { - const [updateRequest, {loading, error}] = useMutation(UPDATE_REQUEST); +function SubmittedCard({ request }: SubmittedCardProps) { + const [updateRequest, { loading, error }] = useMutation(UPDATE_REQUEST); - const noIssues = - No issues found - ; + const noIssues = ( + + No issues found + + ); - return ( - - - - + return ( + + + + -
- -  #{request.request_id} -
-
- {request.item.qtyAvailableForApproval >= request.quantity ? noIssues : noStockWarning(request.item.qtyAvailableForApproval)} - - - - {error ? - Unable to change request status: {error.message} - : ""} - -
- - updateRequest({ - variables: { - updatedRequest: { - request_id: request.request_id, - new_status: DENIED - } - } - })}> - - } - content="Deny request" - /> - updateRequest({ - variables: { - updatedRequest: { - request_id: request.request_id, - new_status: APPROVED - } - } - })}> - - Approve - } - content="Approve request" - /> - -
-
-
- ); +
+ +   + + #{request.request_id} + +
+
+ {request.item.qtyAvailableForApproval >= request.quantity + ? noIssues + : noStockWarning(request.item.qtyAvailableForApproval)} + + + + {error ? ( + + + Unable to change request status: {error.message} + + ) : ( + "" + )} + +
+ + + updateRequest({ + variables: { + updatedRequest: { + request_id: request.request_id, + new_status: DENIED, + }, + }, + }) + } + > + + + } + content="Deny request" + /> + + updateRequest({ + variables: { + updatedRequest: { + request_id: request.request_id, + new_status: APPROVED, + }, + }, + }) + } + > + + Approve + + } + content="Approve request" + /> + +
+
+
+ ); } export default SubmittedCard; diff --git a/client/src/components/desk/submitted/SubmittedList.tsx b/client/src/components/desk/submitted/SubmittedList.tsx index c5945cf..780a391 100644 --- a/client/src/components/desk/submitted/SubmittedList.tsx +++ b/client/src/components/desk/submitted/SubmittedList.tsx @@ -1,43 +1,48 @@ -import React, {Component} from "react"; +import React, { Component } from "react"; +import { Container, Header, Segment } from "semantic-ui-react"; + import CardList from "../CardList"; import SubmittedCard from "./SubmittedCard"; -import {Request} from "../../../types/Request"; -import {Container, Header, Segment} from "semantic-ui-react"; -import {requestSearch} from "../DeskUtil"; +import { Request } from "../../../types/Request"; +import { requestSearch } from "../DeskUtil"; interface SubmittedListProps { - loading: boolean; - requests: Request[]; - subscribeToUpdatedRequests: any; - hidden?: boolean; + loading: boolean; + requests: Request[]; + subscribeToUpdatedRequests: any; + hidden?: boolean; } class SubmittedList extends Component { - componentDidMount(): void { - this.props.subscribeToUpdatedRequests(); - } - - render() { - if (this.props.hidden) { // This is used to hide the submitted list but still call subscribeToUpdatedRequests - return ""; - } + componentDidMount(): void { + this.props.subscribeToUpdatedRequests(); + } - const empty = - -
- Nothing to approve. Take a break! -
-
-
; + render() { + if (this.props.hidden) { + // This is used to hide the submitted list but still call subscribeToUpdatedRequests + return ""; + } - return } - empty={empty}/>; + const empty = ( + + +
Nothing to approve. Take a break!
+
+
+ ); - } + return ( + } + empty={empty} + /> + ); + } } export default SubmittedList; diff --git a/client/src/components/inventory/HardwareCategory.tsx b/client/src/components/inventory/HardwareCategory.tsx index ae491d2..7a1c580 100644 --- a/client/src/components/inventory/HardwareCategory.tsx +++ b/client/src/components/inventory/HardwareCategory.tsx @@ -1,26 +1,23 @@ -import React from 'react'; -import {HwItem} from "../../types/Hardware"; -import HardwareItem from "./HardwareItem"; -import {Item} from "semantic-ui-react"; - -const HardwareCategory = ({name, items, requestsEnabled}: any) => { +import React from "react"; +import { Item } from "semantic-ui-react"; - return ( -
- - {items - // @ts-ignore - .sort((a: any, b: any) => (b.qtyUnreserved > 0) - (a.qtyUnreserved > 0) - || a.item_name.localeCompare(b.item_name)) - .map((item: HwItem) => )} - -
- ); -}; +const HardwareCategory = ({ name, items, requestsEnabled }: any) => ( +
+ + {items + // @ts-ignore + .sort( + (a: any, b: any) => + (b.qtyUnreserved > 0) - (a.qtyUnreserved > 0) || a.item_name.localeCompare(b.item_name) + ) + .map((item: HwItem) => ( + + ))} + +
+); export default HardwareCategory; diff --git a/client/src/components/inventory/HardwareItem.tsx b/client/src/components/inventory/HardwareItem.tsx index f82ebcb..a68ddac 100644 --- a/client/src/components/inventory/HardwareItem.tsx +++ b/client/src/components/inventory/HardwareItem.tsx @@ -1,154 +1,190 @@ -import React, {ChangeEvent} from "react"; -import {Button, Icon, Input, Item, Popup} from "semantic-ui-react"; -import {withToastManager} from "react-toast-notifications"; -import {Link} from "react-router-dom"; -import {HwItem, RequestedItem, SUBMITTED} from "../../types/Hardware"; -import {AppState} from "../../state/Store"; -import {connect} from "react-redux"; -import {User} from "../../types/User"; +import React, { ChangeEvent } from "react"; +import { Button, Icon, Input, Item, Popup } from "semantic-ui-react"; +import { withToastManager } from "react-toast-notifications"; +import { Link } from "react-router-dom"; +import { connect } from "react-redux"; + +import { HwItem, RequestedItem, SUBMITTED } from "../../types/Hardware"; +import { AppState } from "../../state/Store"; +import { User } from "../../types/User"; import RequestButton from "./RequestButton"; interface HardwareItemState { - qtyRequested: number; - loading: boolean; + qtyRequested: number; + loading: boolean; } interface HardwareItemProps { - item: HwItem; - toastManager: any; - requestsEnabled: boolean; - user: User | null; - preview?: boolean; + item: HwItem; + toastManager: any; + requestsEnabled: boolean; + user: User | null; + preview?: boolean; } -function itemImage(src: string, outOfStock: boolean = false) { - - return ; +function itemImage(src: string, outOfStock = false) { + return ( + + ); } class HardwareItem extends React.Component { - constructor(props: HardwareItemProps) { - super(props); - this.state = { - qtyRequested: 1, - loading: false, - }; - } - - public incrementQty = () => { - this.setState({ - qtyRequested: this.state.qtyRequested + 1 - }); - } + constructor(props: HardwareItemProps) { + super(props); + this.state = { + qtyRequested: 1, + loading: false, + }; + } - public decrementQty = () => { - this.setState({ - qtyRequested: this.state.qtyRequested - 1 - }); - } + public incrementQty = () => { + this.setState({ + qtyRequested: this.state.qtyRequested + 1, + }); + }; - public handleQtyUpdate = (qtyInput: ChangeEvent) => { - const qtyAsNumber: number = Number.parseInt(qtyInput.target.value, 10); + public decrementQty = () => { + this.setState({ + qtyRequested: this.state.qtyRequested - 1, + }); + }; - this.updateQtyRequested(qtyAsNumber); - } + public handleQtyUpdate = (qtyInput: ChangeEvent) => { + const qtyAsNumber: number = Number.parseInt(qtyInput.target.value, 10); - public updateQtyRequested = (qtyRequested: number) => { - if (Number.isNaN(qtyRequested)) { - qtyRequested = 0; - } + this.updateQtyRequested(qtyAsNumber); + }; - // Clip qtyRequested to between 0 and maxRequestQty (inclusive) - qtyRequested = Math.min(Math.max(qtyRequested, 0), this.props.item.maxRequestQty); - - this.setState({ - qtyRequested - }); + public updateQtyRequested = (qtyRequested: number) => { + if (Number.isNaN(qtyRequested)) { + qtyRequested = 0; } - public render() { - const newRequest: RequestedItem = { - id: this.props.item.id, - user: this.props.user ? this.props.user.uuid : "", - name: this.props.item.item_name, - qtyRequested: this.state.qtyRequested, - category: this.props.item.category, - status: SUBMITTED, - location: this.props.item.location, - cancelled: false - }; - - const minusBtn = (}> -
- ) : ""; - - const hidden = this.props.user && this.props.item.hidden ? ( - }> - - ) : ""; - - return ( - - {itemImage(this.props.item.imageUrl, this.props.item.qtyUnreserved <= 0)} - - {editBtn} {hidden} {this.props.item.item_name || (this.props.preview && "Item Name")} - {maxPerRequest} - {this.props.item.description || (this.props.preview && "Description")} - {qtyRequest} - - - ); - } + // Clip qtyRequested to between 0 and maxRequestQty (inclusive) + qtyRequested = Math.min(Math.max(qtyRequested, 0), this.props.item.maxRequestQty); + + this.setState({ + qtyRequested, + }); + }; + + public render() { + const newRequest: RequestedItem = { + id: this.props.item.id, + user: this.props.user ? this.props.user.uuid : "", + name: this.props.item.item_name, + qtyRequested: this.state.qtyRequested, + category: this.props.item.category, + status: SUBMITTED, + location: this.props.item.location, + cancelled: false, + }; + + const minusBtn = ( + + } + /> + ) : ( + "" + ); + + const hidden = + this.props.user && this.props.item.hidden ? ( + } + /> + ) : ( + "" + ); + + return ( + + {itemImage(this.props.item.imageUrl, this.props.item.qtyUnreserved <= 0)} + + + {editBtn} {hidden} {this.props.item.item_name || (this.props.preview && "Item Name")} + + {maxPerRequest} + + {this.props.item.description || (this.props.preview && "Description")} + + {qtyRequest} + + + ); + } } function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } export default withToastManager(connect(mapStateToProps)(HardwareItem)); - - - diff --git a/client/src/components/inventory/HardwareLocation.tsx b/client/src/components/inventory/HardwareLocation.tsx index 0c290b6..e522f9e 100644 --- a/client/src/components/inventory/HardwareLocation.tsx +++ b/client/src/components/inventory/HardwareLocation.tsx @@ -1,14 +1,14 @@ -import React from 'react'; -import {Header, Icon} from "semantic-ui-react"; +import React from "react"; +import { Header, Icon } from "semantic-ui-react"; -const HardwareLocation = ({location_name}: any) => { - return ( -
-
{location_name}
-

These items are available from {location_name}. Pick them up and return them - there.

-
- ); -}; +const HardwareLocation = ({ location_name }: any) => ( +
+
+ + {location_name} +
+

These items are available from {location_name}. Pick them up and return them there.

+
+); export default HardwareLocation; diff --git a/client/src/components/inventory/HardwareLocationContents.tsx b/client/src/components/inventory/HardwareLocationContents.tsx index 04fd127..9ebc2ee 100644 --- a/client/src/components/inventory/HardwareLocationContents.tsx +++ b/client/src/components/inventory/HardwareLocationContents.tsx @@ -1,86 +1,113 @@ -import React, {useState} from 'react'; +import React, { useState } from "react"; +import { Accordion, Divider, Header, Icon } from "semantic-ui-react"; +import { connect } from "react-redux"; + import HardwareLocation from "./HardwareLocation"; -import {Accordion, Divider, Header, Icon} from "semantic-ui-react"; -import {HwItem, ItemByCat, ItemByLocation} from "../../types/Hardware"; +import { HwItem, ItemByCat, ItemByLocation } from "../../types/Hardware"; import HardwareCategory from "./HardwareCategory"; import NoItemsFound from "./NoItemsFound"; -import {AppState} from "../../state/Store"; -import {connect} from "react-redux"; +import { AppState } from "../../state/Store"; function handleClick(e: any, titleProps: any, accordionState: any, setAccordionState: any): void { - const {index} = titleProps; - const accordionStateClone = accordionState.slice(0); - - if (accordionStateClone.indexOf(index) > -1) { - accordionStateClone.splice(accordionStateClone.indexOf(index), 1); - } else { - accordionStateClone.push(index); - } + const { index } = titleProps; + const accordionStateClone = accordionState.slice(0); - setAccordionState(accordionStateClone); + if (accordionStateClone.indexOf(index) > -1) { + accordionStateClone.splice(accordionStateClone.indexOf(index), 1); + } else { + accordionStateClone.push(index); + } + setAccordionState(accordionStateClone); } function filteredItems(items: HwItem[], searchQuery: string): HwItem[] { - return items.filter((item: HwItem) => item.item_name.toLowerCase().includes(searchQuery)); + return items.filter((item: HwItem) => item.item_name.toLowerCase().includes(searchQuery)); } function combinedAndFilteredItemsByCategory(categories: any, searchQuery: string): HwItem[] { - return filteredItems(categories.reduce((acc: any, val: any) => acc.concat(val.items), []), searchQuery); + return filteredItems( + categories.reduce((acc: any, val: any) => acc.concat(val.items), []), + searchQuery + ); } interface HardwareListProps { - itemsByLocation: ItemByLocation, - searchQuery: string, - requestsEnabled: boolean + itemsByLocation: ItemByLocation; + searchQuery: string; + requestsEnabled: boolean; } -const HardwareLocationContents = ({itemsByLocation, searchQuery, requestsEnabled}: HardwareListProps) => { - const [accordionState, setAccordionState] = useState([0]); +const HardwareLocationContents = ({ + itemsByLocation, + searchQuery, + requestsEnabled, +}: HardwareListProps) => { + const [accordionState, setAccordionState] = useState([0]); - return ( -
- - - {itemsByLocation.categories.map((itemByCat: ItemByCat, index: number) => <> - {filteredItems(itemByCat.items, searchQuery).length ? <> - = 3} - index={index} - onClick={(e: any, titleProps: any) => { - handleClick(e, titleProps, accordionState, setAccordionState); - }}> -
- - {itemByCat.category.category_name} -
-
- = 3} - index={index}> - - : ""} - )} - {(!itemsByLocation.categories.length - || !combinedAndFilteredItemsByCategory(itemsByLocation.categories, searchQuery).length) && - } -
- -
- ); + return ( +
+ + + {itemsByLocation.categories.map((itemByCat: ItemByCat, index: number) => ( + <> + {filteredItems(itemByCat.items, searchQuery).length ? ( + <> + = 3} + index={index} + onClick={(e: any, titleProps: any) => { + handleClick(e, titleProps, accordionState, setAccordionState); + }} + > +
+ + {itemByCat.category.category_name} +
+
+ = 3} + index={index} + > + + + + ) : ( + "" + )} + + ))} + {(!itemsByLocation.categories.length || + !combinedAndFilteredItemsByCategory(itemsByLocation.categories, searchQuery).length) && ( + + )} +
+ +
+ ); }; function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } export default connect(mapStateToProps)(HardwareLocationContents); diff --git a/client/src/components/inventory/NewHardwareList.tsx b/client/src/components/inventory/NewHardwareList.tsx index ada161f..394adc0 100644 --- a/client/src/components/inventory/NewHardwareList.tsx +++ b/client/src/components/inventory/NewHardwareList.tsx @@ -1,128 +1,127 @@ -import React, {useState} from 'react'; -import {useQuery} from "@apollo/client"; -import {ALL_ITEMS, GET_SETTING} from "../util/graphql/Queries"; -import { - Button, - Grid, - Header, - Icon, - Input, - Loader, - Message -} from "semantic-ui-react"; -import {ItemByLocation} from "../../types/Hardware"; -import HardwareLocationContents from "./HardwareLocationContents"; -import {connect} from "react-redux"; -import {AppState} from "../../state/Store"; -import {User} from "../../types/User"; -import {Link} from "react-router-dom"; +import React, { useState } from "react"; +import { useQuery } from "@apollo/client"; +import { Button, Grid, Header, Icon, Input, Loader, Message } from "semantic-ui-react"; +import { connect } from "react-redux"; +import { Link } from "react-router-dom"; -const NewHardwareList = ({user}: { user: User | null }) => { - const {data, loading, error} = useQuery(ALL_ITEMS); - const [searchQuery, setSearchQuery] = useState(""); +import { ALL_ITEMS, GET_SETTING } from "../util/graphql/Queries"; +import { ItemByLocation } from "../../types/Hardware"; +import HardwareLocationContents from "./HardwareLocationContents"; +import { AppState } from "../../state/Store"; +import { User } from "../../types/User"; - const setting = useQuery(GET_SETTING, { - variables: {settingName: "requests_allowed"} - }); +const NewHardwareList = ({ user }: { user: User | null }) => { + const { data, loading, error } = useQuery(ALL_ITEMS); + const [searchQuery, setSearchQuery] = useState(""); - if (loading || setting.loading) { - return ( - <> -
Inventory
- - ); - } + const setting = useQuery(GET_SETTING, { + variables: { settingName: "requests_allowed" }, + }); - if (error) { - return <> -
Inventory
- - Error displaying hardware - inventory -

Try refreshing the page. If that doesn't work, - contact a member of the HackGT Team for - assistance.

-
- ; - } + if (loading || setting.loading) { + return ( + <> +
Inventory
+ + + ); + } - let requestsEnabled = true; - if(!setting.error && setting.data.setting !== undefined) { - requestsEnabled = (setting.data.setting.value === "true"); - } + if (error) { + return ( + <> +
Inventory
+ + Error displaying hardware inventory +

+ Try refreshing the page. If that doesn't work, contact a member of the HackGT Team for + assistance. +

+
+ + ); + } - let noRequestsMessageText = ""; - if (!requestsEnabled) { - noRequestsMessageText = "Hardware checkout requests can't be made at this time."; - } else if (requestsEnabled && !user) { - noRequestsMessageText = "Sign in to request hardware."; - } + let requestsEnabled = true; + if (!setting.error && setting.data.setting !== undefined) { + requestsEnabled = setting.data.setting.value === "true"; + } - const noRequestsMessage = !requestsEnabled || !user ? ( - - - - Look, but do not touch - {noRequestsMessageText} - - - ) : ""; + let noRequestsMessageText = ""; + if (!requestsEnabled) { + noRequestsMessageText = "Hardware checkout requests can't be made at this time."; + } else if (requestsEnabled && !user) { + noRequestsMessageText = "Sign in to request hardware."; + } - return ( -
- - -
Inventory
-
- - {user && user.admin ? - : ""} - -
- - {noRequestsMessage} - - - { - if (value.length >= 3) { - setSearchQuery(value.trim().toLowerCase()); - } else { - setSearchQuery(""); - } - } - } - /> - - - - {data.allItems.map((itemsByLocation: ItemByLocation) => - - ) - } -
+ const noRequestsMessage = + !requestsEnabled || !user ? ( + + + + Look, but do not touch + {noRequestsMessageText} + + + + ) : ( + "" ); + + return ( +
+ + +
Inventory
+
+ + {user && user.admin ? ( + + ) : ( + "" + )} + +
+ + {noRequestsMessage} + + + { + if (value.length >= 3) { + setSearchQuery(value.trim().toLowerCase()); + } else { + setSearchQuery(""); + } + }} + /> + + + + {data.allItems.map((itemsByLocation: ItemByLocation) => ( + + ))} +
+ ); }; function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } export default connect(mapStateToProps)(NewHardwareList); diff --git a/client/src/components/inventory/NoItemsFound.tsx b/client/src/components/inventory/NoItemsFound.tsx index 38595d0..472e3c4 100644 --- a/client/src/components/inventory/NoItemsFound.tsx +++ b/client/src/components/inventory/NoItemsFound.tsx @@ -1,39 +1,50 @@ -import React from 'react'; -import {Button, Header, Icon, Message, Segment} from "semantic-ui-react"; -import {Link} from "react-router-dom"; -import {AppState} from "../../state/Store"; -import {connect} from "react-redux"; +import React from "react"; +import { Button, Header, Icon, Message, Segment } from "semantic-ui-react"; +import { Link } from "react-router-dom"; +import { connect } from "react-redux"; -const NoItemsFound = ({searchQuery, user}: any) => { - console.log("user", user); - if (!searchQuery) { - return ( - No items here! -

This location doesn't have any items you can see. Try again later, or contact a HackGT staff member for - further assistance.

- {user && user.admin && - <> - - - } -
); - } +import { AppState } from "../../state/Store"; - return ( -
- - No matching items were found -
- If you're trying to find something specific, you can ask a staff member at the HackGT - hardware desk staff for help! -
+const NoItemsFound = ({ searchQuery, user }: any) => { + console.log("user", user); + if (!searchQuery) { + return ( + + No items here! +

+ This location doesn't have any items you can see. Try again later, or contact a HackGT + staff member for further assistance. +

+ {user && user.admin && ( + <> + + + + )} +
); + } + + return ( + +
+ + No matching items were found +
+ If you're trying to find something specific, you can ask a staff member at the HackGT hardware + desk staff for help! +
+ ); }; function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } export default connect(mapStateToProps)(NoItemsFound); diff --git a/client/src/components/inventory/RequestButton.tsx b/client/src/components/inventory/RequestButton.tsx index fec0722..a2df4d7 100644 --- a/client/src/components/inventory/RequestButton.tsx +++ b/client/src/components/inventory/RequestButton.tsx @@ -1,86 +1,101 @@ import React from "react"; -import {Button, Icon, Loader, Message} from "semantic-ui-react"; -import {useMutation} from "@apollo/client"; -import {Query} from "@apollo/client/react/components"; -import {CREATE_REQUEST} from "../util/graphql/Mutations"; -import {RequestedItem} from "../../types/Hardware"; -import {withToastManager} from "react-toast-notifications"; -import {GET_SETTING, USER_REQUESTS} from "../util/graphql/Queries"; -import {User} from "../../types/User"; +import { Button, Icon, Loader, Message } from "semantic-ui-react"; +import { useMutation } from "@apollo/client"; +import { Query } from "@apollo/client/react/components"; +import { withToastManager } from "react-toast-notifications"; + +import { CREATE_REQUEST } from "../util/graphql/Mutations"; +import { RequestedItem } from "../../types/Hardware"; +import { GET_SETTING, USER_REQUESTS } from "../util/graphql/Queries"; +import { User } from "../../types/User"; interface RequestButtonProps { - requestedItem: RequestedItem, - user: User, - toastManager: any + requestedItem: RequestedItem; + user: User; + toastManager: any; } -function RequestButton({requestedItem, user, toastManager}: RequestButtonProps) { - const [createRequest, {loading, error}] = useMutation(CREATE_REQUEST, { - refetchQueries: [ - { - query: USER_REQUESTS, - variables: { - uuid: user.uuid - }, - }, - ], - }); +function RequestButton({ requestedItem, user, toastManager }: RequestButtonProps) { + const [createRequest, { loading, error }] = useMutation(CREATE_REQUEST, { + refetchQueries: [ + { + query: USER_REQUESTS, + variables: { + uuid: user.uuid, + }, + }, + ], + }); - if (loading) { - return ; - } - if (error) { - return ; - } - let requests_allowed = "true"; + if (loading) { + return ; + } + if (error) { return ( - - { - ({loading, error, data}: any) => { - if (loading) { - return ; - } - if (!error && data.setting !== undefined) { - requests_allowed = data.setting.value; - } - return - } - } - + ); + } + let requests_allowed = "true"; + return ( + + {({ loading, error, data }: any) => { + if (loading) { + return ; + } + if (!error && data.setting !== undefined) { + requests_allowed = data.setting.value; + } + return ( + + ); + }} + + ); } export default withToastManager(RequestButton); diff --git a/client/src/components/item/CreateItemWrapper.tsx b/client/src/components/item/CreateItemWrapper.tsx index 4508be8..c5bbd26 100644 --- a/client/src/components/item/CreateItemWrapper.tsx +++ b/client/src/components/item/CreateItemWrapper.tsx @@ -1,44 +1,41 @@ -import React, {Component} from "react"; -import {connect} from "react-redux"; -import {AppState} from "../../state/Store"; +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { match } from "react-router"; +import { Header } from "semantic-ui-react"; + +import { AppState } from "../../state/Store"; import ItemEditForm from "./ItemEditForm"; -import {match} from "react-router"; -import {Header} from "semantic-ui-react"; interface CreateItemProps { - match: match & CreateItemParams; + match: match & CreateItemParams; } interface CreateItemParams { - params: { itemId: string }; + params: { itemId: string }; } -interface CreateItemState { -} +interface CreateItemState {} class CreateItemWrapper extends Component { - constructor(props: CreateItemProps) { - super(props); - this.state = {}; - - } - - public render() { - return ( -
-
Create Item
- -
- ); - } + constructor(props: CreateItemProps) { + super(props); + this.state = {}; + } + + public render() { + return ( +
+
Create Item
+ +
+ ); + } } function mapStateToProps(state: AppState) { - return { - user: state.account - }; + return { + user: state.account, + }; } -export default connect( - mapStateToProps -)(CreateItemWrapper); +export default connect(mapStateToProps)(CreateItemWrapper); diff --git a/client/src/components/item/EditItemWrapper.tsx b/client/src/components/item/EditItemWrapper.tsx index 4adc634..34335d3 100644 --- a/client/src/components/item/EditItemWrapper.tsx +++ b/client/src/components/item/EditItemWrapper.tsx @@ -1,61 +1,50 @@ -import React, {Component} from "react"; -import {match} from "react-router"; -import {Query} from "@apollo/client/react/components"; +import React, { Component } from "react"; +import { match } from "react-router"; +import { Query } from "@apollo/client/react/components"; +import { Header, Loader, Message } from "semantic-ui-react"; + import ItemEditForm from "./ItemEditForm"; -import {Header, Loader, Message} from "semantic-ui-react"; -import {ITEM_EDIT_GET_ITEM} from "../util/graphql/Queries"; +import { ITEM_EDIT_GET_ITEM } from "../util/graphql/Queries"; interface EditItemProps { - match: match & EditItemParams; + match: match & EditItemParams; } interface EditItemParams { - params: { itemId: string }; + params: { itemId: string }; } interface EditItemState { - item_name: string; + item_name: string; } class EditItemWrapper extends Component { - constructor(props: EditItemProps) { - super(props); - this.state = { - item_name: "" - }; - } - - - public render() { - const itemId: number = parseInt(this.props.match.params.itemId, 10); - return ( -
-
Edit Item
- - { - ({loading, error, data}: any) => { - if (loading) { - return ; - } else if (error) { - return ; - } - return ; - - } - } - -
- ); - } + constructor(props: EditItemProps) { + super(props); + this.state = { + item_name: "", + }; + } + + public render() { + const itemId: number = parseInt(this.props.match.params.itemId, 10); + return ( +
+
Edit Item
+ + {({ loading, error, data }: any) => { + if (loading) { + return ; + } + if (error) { + return ; + } + return ; + }} + +
+ ); + } } export default EditItemWrapper; diff --git a/client/src/components/item/ItemEditForm.tsx b/client/src/components/item/ItemEditForm.tsx index 92c6d46..9466695 100644 --- a/client/src/components/item/ItemEditForm.tsx +++ b/client/src/components/item/ItemEditForm.tsx @@ -1,434 +1,536 @@ -import React, {ChangeEvent, Component, FormEvent} from "react"; -import HardwareItem from "../inventory/HardwareItem"; -import {Button, CheckboxProps, DropdownProps, Form, Grid, Header, Item, Label, Message, Popup} from "semantic-ui-react"; +import React, { ChangeEvent, Component, FormEvent } from "react"; +import { + Button, + CheckboxProps, + DropdownProps, + Form, + Grid, + Header, + Item, + Label, + Message, + Popup, +} from "semantic-ui-react"; +import { withToastManager } from "react-toast-notifications"; +import { Redirect } from "react-router"; +import { Mutation, Query } from "@apollo/client/react/components"; + import AddOptionDropdown from "../util/AddOptionDropdown"; -import {withToastManager} from "react-toast-notifications"; -import {Redirect} from "react-router"; -import {Mutation, Query} from "@apollo/client/react/components"; -import {CREATE_ITEM, UPDATE_ITEM} from "../util/graphql/Mutations"; -import {ALL_CATEGORIES, ALL_ITEMS, ALL_LOCATIONS} from "../util/graphql/Queries"; -import {ItemNoId, Location} from "../../types/Hardware"; +import HardwareItem from "../inventory/HardwareItem"; +import { CREATE_ITEM, UPDATE_ITEM } from "../util/graphql/Mutations"; +import { ALL_CATEGORIES, ALL_ITEMS, ALL_LOCATIONS } from "../util/graphql/Queries"; +import { ItemNoId, Location } from "../../types/Hardware"; interface ItemDetails { - approvalRequired: boolean; - returnRequired: boolean; - price: number; - hidden: boolean; - owner: string; + approvalRequired: boolean; + returnRequired: boolean; + price: number; + hidden: boolean; + owner: string; } interface ItemEditProps { - preloadItemId: number; - preloadItem: ItemComplete; - createItem: boolean; - toastManager: any; - loading?: boolean; + preloadItemId: number; + preloadItem: ItemComplete; + createItem: boolean; + toastManager: any; + loading?: boolean; } -export type ItemComplete = ItemNoId & ItemDetails & { +export type ItemComplete = ItemNoId & + ItemDetails & { [name: string]: any | any[]; -}; + }; export type Category = { - category_id: number; - category_name: string; + category_id: number; + category_name: string; }; interface ItemEditState { - loading: boolean; - item: ItemComplete; - itemOwnerChoices: string[]; - itemPreviewKey: number; - categoryError: boolean; - ownerError: boolean; - locationError: boolean; - qtyPerRequestTooLargeError: boolean; + loading: boolean; + item: ItemComplete; + itemOwnerChoices: string[]; + itemPreviewKey: number; + categoryError: boolean; + ownerError: boolean; + locationError: boolean; + qtyPerRequestTooLargeError: boolean; } class ItemEditForm extends Component { - constructor(props: ItemEditProps) { - super(props); - this.state = { - categoryError: false, - ownerError: false, - locationError: false, - qtyPerRequestTooLargeError: false, - loading: false, - item: this.props.createItem ? { - item_name: "", - description: "", - imageUrl: "", - category: "", - location: "", - totalAvailable: 0, - maxRequestQty: 0, - qtyAvailableForApproval: 0, - qtyUnreserved: 0, - qtyInStock: 0, - price: 0, - approvalRequired: true, - returnRequired: true, - hidden: false, - owner: "HackGT" - } : this.props.preloadItem, - itemOwnerChoices: ["HackGT", "The Hive", "Invention Studio"], - itemPreviewKey: 0 - }; - + constructor(props: ItemEditProps) { + super(props); + this.state = { + categoryError: false, + ownerError: false, + locationError: false, + qtyPerRequestTooLargeError: false, + loading: false, + item: this.props.createItem + ? { + item_name: "", + description: "", + imageUrl: "", + category: "", + location: "", + totalAvailable: 0, + maxRequestQty: 0, + qtyAvailableForApproval: 0, + qtyUnreserved: 0, + qtyInStock: 0, + price: 0, + approvalRequired: true, + returnRequired: true, + hidden: false, + owner: "HackGT", + } + : this.props.preloadItem, + itemOwnerChoices: ["HackGT", "The Hive", "Invention Studio"], + itemPreviewKey: 0, + }; + } + + public handleInputChangeCheckbox = ( + event: FormEvent, + checkboxProps: CheckboxProps + ): void => { + if (checkboxProps && checkboxProps.name && typeof checkboxProps.checked !== "undefined") { + const value: boolean = checkboxProps.checked; + const { name } = checkboxProps; + + // @ts-ignore + this.setState({ + item: { + ...this.state.item, + [name]: value, + }, + }); } - - public handleInputChangeCheckbox = (event: FormEvent, checkboxProps: CheckboxProps): void => { - if (checkboxProps && checkboxProps.name && typeof checkboxProps.checked !== "undefined") { - const value: boolean = checkboxProps.checked; - const name: string = checkboxProps.name; - - // @ts-ignore - this.setState({ - item: { - ...this.state.item, - [name]: value - } - }); - } + }; + + public handleInputChangeDropdown = ( + event: React.SyntheticEvent, + data: DropdownProps, + inputName: string + ): void => { + const { value } = data; + // @ts-ignore + this.setState({ + item: { + ...this.state.item, + [inputName]: value, + }, + }); + }; + + public handleInputChange = (event: ChangeEvent): void => { + const { target } = event; + let { value } = target; + const { name } = target; + const inputType = target.type; + + // Convert number input values to numbers + if (inputType === "number") { + value = Number.parseFloat(value); } - public handleInputChangeDropdown = (event: React.SyntheticEvent, data: DropdownProps, inputName: string): void => { - const value = data.value; - // @ts-ignore - this.setState({ - item: { - ...this.state.item, - [inputName]: value - } - }); + if (name === "totalAvailable") { + this.setState({ + itemPreviewKey: this.state.itemPreviewKey + 1, + }); } - public handleInputChange = (event: ChangeEvent): void => { - const target = event.target; - let value: string | number = target.value; - const name = target.name; - const inputType = target.type; - - // Convert number input values to numbers - if (inputType === "number") { - value = Number.parseFloat(value); - } - - if (name === "totalAvailable") { - this.setState({ - itemPreviewKey: this.state.itemPreviewKey + 1 - }); - } - - // @ts-ignore - this.setState({ - item: { - ...this.state.item, - [name]: value - } - }); - } - - public render() { - const itemOwnerChoices = ["HackGT", "The Hive", "Invention Studio"]; - - const qtyPerRequestTooBigErrorMessage = ; - - - return ( - - - - - {(submitForm: any, {loading, error, data}: any) => ( -
{ - e.preventDefault(); - const {toastManager} = this.props; - - let variables: any = {newItem: this.state.item}; - - if (!this.props.createItem) { - variables = { - itemId: this.props.preloadItemId, - updatedItem: this.state.item - }; - if (typeof variables.updatedItem.location === "object") { // Transform location from the object from server into just the location name - variables.updatedItem.location = variables.updatedItem.location.location_name; - } - delete variables.updatedItem.__typename; - delete variables.updatedItem.qtyAvailableForApproval; - delete variables.updatedItem.qtyInStock; - delete variables.updatedItem.qtyUnreserved; - } else { - if (typeof variables.newItem.location === "object") { // Transform location from the object from server into just the location name - variables.newItem.location = variables.newItem.location.location_name; - } - delete variables.newItem.__typename; - delete variables.newItem.qtyAvailableForApproval; - delete variables.newItem.qtyInStock; - delete variables.newItem.qtyUnreserved; - } - const categoryError = this.state.item.category === ""; - const locationError = this.state.item.location === ""; - const ownerError = this.state.item.owner === ""; - const qtyPerRequestTooLargeError = this.state.item.maxRequestQty > this.state.item.totalAvailable; - - this.setState({ - categoryError, - locationError, - ownerError, - qtyPerRequestTooLargeError - }); - - if (categoryError || locationError || ownerError || qtyPerRequestTooLargeError) { - return; - } - - submitForm({ - variables - }).then(() => { - toastManager.add(`${this.state.item.item_name} saved`, { - appearance: "success", - autoDismiss: true, - placement: "top-center" - }); - }).catch((err: Error) => { - toastManager.add(`Couldn't save your item because of an error: ${err.message}`, { - appearance: "error", - autoDismiss: false, - placement: "top-center" - }); - console.error(err); - }); - }}> - - {data && !error ? : ""} - - - - - - - - - - - {(result: any) => { - const queryLoading = result.loading; - const queryError = result.error; - const queryData = result.data; - let categoriesList: string[] = []; - let dataLoadedKey = 0; // this allows us to "reset" the AddOptionDropdown when we - // have the list of existing categories it should show. - // See https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key - // for more information on why this is necessary and why it works. - if (queryData && queryData.categories) { - categoriesList = queryData.categories.map((item: Category) => item.category_name); - dataLoadedKey = 1; - } - const queryErrorMsg = ; - return (
- {queryError ? queryErrorMsg : ""} - -
); - }} -
- -
- - - - - - - -
- - - - - - {(result: any) => { - const queryLoading = result.loading; - const queryError = result.error; - const queryData = result.data; - let locationsList: string[] = []; - let dataLoadedKey = 0; // this allows us to "reset" the AddOptionDropdown when we - // have the list of existing locations it should show. - // See https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key - // for more information on why this is necessary and why it works. - if (queryData && queryData.locations) { - locationsList = queryData.locations.map((item: Location) => item.location_name); - dataLoadedKey = 1; - } - const queryErrorMsg = ; - - return (
- {queryError ? queryErrorMsg : ""} - -
); - }} -
- -
-
- -

Stock

- {this.state.qtyPerRequestTooLargeError ? qtyPerRequestTooBigErrorMessage : ""} - - - - - - - - -

Calculated Quantities

- - - Unreserved} - content="The number of an item that is not reserved"/> -

{this.state.item.qtyUnreserved}

-
- - In stock} - content="The number of an item that should be physically at the hardware desk"/> -

{this.state.item.qtyInStock}

-
- - Available for approval} - content="The number of an item that is available to be allocated to requests waiting to be approved"/> -

{this.state.item.qtyAvailableForApproval}

-
-
- {/**/} - -

Checkout Controls

- - } - content="Whether users who check out this item are expected to return it"/> - } - content="Whether hardware checkout staff must approve requests for this item"/> - } - content="Whether to hide this item on the public list of hardware"/> - - - - - )} -
-
- -
Preview
- - + ); + + return ( + + + + + {(submitForm: any, { loading, error, data }: any) => ( +
{ + e.preventDefault(); + const { toastManager } = this.props; + + let variables: any = { newItem: this.state.item }; + + if (!this.props.createItem) { + variables = { + itemId: this.props.preloadItemId, + updatedItem: this.state.item, + }; + if (typeof variables.updatedItem.location === "object") { + // Transform location from the object from server into just the location name + variables.updatedItem.location = + variables.updatedItem.location.location_name; + } + delete variables.updatedItem.__typename; + delete variables.updatedItem.qtyAvailableForApproval; + delete variables.updatedItem.qtyInStock; + delete variables.updatedItem.qtyUnreserved; + } else { + if (typeof variables.newItem.location === "object") { + // Transform location from the object from server into just the location name + variables.newItem.location = variables.newItem.location.location_name; + } + delete variables.newItem.__typename; + delete variables.newItem.qtyAvailableForApproval; + delete variables.newItem.qtyInStock; + delete variables.newItem.qtyUnreserved; + } + const categoryError = this.state.item.category === ""; + const locationError = this.state.item.location === ""; + const ownerError = this.state.item.owner === ""; + const qtyPerRequestTooLargeError = + this.state.item.maxRequestQty > this.state.item.totalAvailable; + + this.setState({ + categoryError, + locationError, + ownerError, + qtyPerRequestTooLargeError, + }); + + if ( + categoryError || + locationError || + ownerError || + qtyPerRequestTooLargeError + ) { + return; + } + + submitForm({ + variables, + }) + .then(() => { + toastManager.add(`${this.state.item.item_name} saved`, { + appearance: "success", + autoDismiss: true, + placement: "top-center", + }); + }) + .catch((err: Error) => { + toastManager.add( + `Couldn't save your item because of an error: ${err.message}`, + { + appearance: "error", + autoDismiss: false, + placement: "top-center", + } + ); + console.error(err); + }); + }} + > + {data && !error ? : ""} + + + + + + + + + + {(result: any) => { + const queryLoading = result.loading; + const queryError = result.error; + const queryData = result.data; + let categoriesList: string[] = []; + let dataLoadedKey = 0; // this allows us to "reset" the AddOptionDropdown when we + // have the list of existing categories it should show. + // See https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key + // for more information on why this is necessary and why it works. + if (queryData && queryData.categories) { + categoriesList = queryData.categories.map( + (item: Category) => item.category_name + ); + dataLoadedKey = 1; + } + const queryErrorMsg = ( + - - - - - ); - } + ); + return ( +
+ {queryError ? queryErrorMsg : ""} + +
+ ); + }} +
+
+ + + + + + + +
+ + + + + {(result: any) => { + const queryLoading = result.loading; + const queryError = result.error; + const queryData = result.data; + let locationsList: string[] = []; + let dataLoadedKey = 0; // this allows us to "reset" the AddOptionDropdown when we + // have the list of existing locations it should show. + // See https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key + // for more information on why this is necessary and why it works. + if (queryData && queryData.locations) { + locationsList = queryData.locations.map( + (item: Location) => item.location_name + ); + dataLoadedKey = 1; + } + const queryErrorMsg = ( + + ); + + return ( +
+ {queryError ? queryErrorMsg : ""} + +
+ ); + }} +
+
+
+ +

Stock

+ {this.state.qtyPerRequestTooLargeError ? qtyPerRequestTooBigErrorMessage : ""} + + + + + + + + +

Calculated Quantities

+ + + Unreserved} + content="The number of an item that is not reserved" + /> +

{this.state.item.qtyUnreserved}

+
+ + In stock} + content="The number of an item that should be physically at the hardware desk" + /> +

{this.state.item.qtyInStock}

+
+ + Available for approval} + content="The number of an item that is available to be allocated to requests waiting to be approved" + /> +

{this.state.item.qtyAvailableForApproval}

+
+
+ {/* */} + +

Checkout Controls

+ + + } + content="Whether users who check out this item are expected to return it" + /> + + } + content="Whether hardware checkout staff must approve requests for this item" + /> + + } + content="Whether to hide this item on the public list of hardware" + /> + + + + )} +
+
+ +
Preview
+ + + +
+
+
+ ); + } } export default withToastManager(ItemEditForm); diff --git a/client/src/components/item/ItemWrapper.tsx b/client/src/components/item/ItemWrapper.tsx index 0030ff1..2065d4d 100644 --- a/client/src/components/item/ItemWrapper.tsx +++ b/client/src/components/item/ItemWrapper.tsx @@ -1,20 +1,25 @@ -import React, {Component} from "react"; -import {match, Switch} from "react-router"; +import React, { Component } from "react"; +import { match, Switch } from "react-router"; + import CreateItemWrapper from "./CreateItemWrapper"; import PrivateRoute from "../util/PrivateRoute"; import EditItemWrapper from "./EditItemWrapper"; class ItemWrapper extends Component<{ match: match }> { - public render() { - return ( -
- - - - -
- ); - } + public render() { + return ( +
+ + + + +
+ ); + } } export default ItemWrapper; diff --git a/client/src/components/reports/demand/ItemDemandReport.tsx b/client/src/components/reports/demand/ItemDemandReport.tsx index 58249eb..0b304e3 100644 --- a/client/src/components/reports/demand/ItemDemandReport.tsx +++ b/client/src/components/reports/demand/ItemDemandReport.tsx @@ -1,192 +1,231 @@ -import React, {useMemo} from 'react'; -import {useQuery} from "@apollo/client"; -import {DETAILED_ITEM_STATISTICS} from "../../util/graphql/Queries"; -import {Header, List, Message, Table} from "semantic-ui-react"; +import React, { useMemo } from "react"; +import { useQuery } from "@apollo/client"; +import { Header, List, Message, Table } from "semantic-ui-react"; import DataTable from "react-data-table-component"; + +import { DETAILED_ITEM_STATISTICS } from "../../util/graphql/Queries"; import LoadingSpinner from "../../util/LoadingSpinner"; import { - ABANDONED, - APPROVED, - CANCELLED, - DAMAGED, - DENIED, - FULFILLED, - ItemWithStatistics, - LOST, - RETURNED + ABANDONED, + APPROVED, + CANCELLED, + DAMAGED, + DENIED, + FULFILLED, + ItemWithStatistics, + LOST, + RETURNED, } from "../../../types/Hardware"; function ColumnDef(column: string, def: string) { - return {column, def}; + return { column, def }; } function customSort(rows: any[], field: string, direction: string) { - console.log(rows, field, direction); - // const splitField = field.split(".") - + console.log(rows, field, direction); + // const splitField = field.split(".") - return rows; + return rows; } - function ItemDemandReport(props: {}) { - const {data, loading, error} = useQuery(DETAILED_ITEM_STATISTICS, { - partialRefetch: true - }); - - const columns = useMemo(() => [ - { - name: "Location", - selector: "item.location.location_name", - sortable: true, - grow: 2 - - }, - { - name: "Item", - selector: "item.item_name", - sortable: true, - grow: 2 - }, - { - name: "Total Available", - selector: "item.totalAvailable", - sortable: true, - center: true - }, - { - name: "Demand", - selector: "totalDemand", - sortable: true, - center: true - }, - { - name: "Qty Approved", - selector: "totalApproved", - sortable: true, - center: true - }, - { - name: "Qty Fulfilled", - selector: "totalFulfilled", - sortable: true, - center: true - }, - { - name: "Qty Not Fulfilled", - selector: "totalNotFulfilled", - sortable: true, - center: true - }, - { - name: "Qty Returned", - selector: "totalReturned", - sortable: true, - center: true - }, - { - name: "Qty Lost/Damaged", - selector: "totalLostDamaged", - sortable: true, - center: true - } - ], []); - - const totalRequests = loading ? 0 : data.itemStatistics.reduce((prev: number, item: ItemWithStatistics) => - prev + item.detailedQuantities.total, 0); - - function sumQtys(itemData: ItemWithStatistics, statuses: string[]) { - return statuses.reduce((accumulator: number, status: string) => itemData.detailedQuantities[status] + accumulator, 0); - } - - function calculatePartial(itemData: ItemWithStatistics, statuses: string[], totalAvailable: number) { - const itemTotal = sumQtys(itemData, statuses); - const percent = (itemTotal / totalAvailable * 100).toPrecision(3); - - return `${itemTotal} (${percent}%)` - } - - const calculatedData = loading || totalRequests === 0 ? [] : data.itemStatistics.map((itemData: ItemWithStatistics) => { - const totalAvailable = itemData.item.totalAvailable; - return { - ...itemData, - percentAllRequests: `${itemData.detailedQuantities.total / totalRequests * 100}%`, - totalDemand: `${itemData.detailedQuantities.total} (${(itemData.detailedQuantities.total / itemData.item.totalAvailable * 100).toPrecision(3)}%)`, - totalApproved: calculatePartial(itemData, [APPROVED, FULFILLED, RETURNED], totalAvailable), - totalFulfilled: calculatePartial(itemData, [FULFILLED, RETURNED], totalAvailable), - totalNotFulfilled: sumQtys(itemData, [ABANDONED, CANCELLED, DENIED]), - totalReturned: sumQtys(itemData, [RETURNED]), - totalLostDamaged: calculatePartial(itemData, [LOST, DAMAGED], totalAvailable) - } - }); - - if (error) { - return <> -
- - Error displaying report -

Something is preventing us from showing this report: {error.message}

-
- ; - } - - const columnDefs = [ - ColumnDef("Demand", "The total quantity of this item that has ever been requested"), - ColumnDef("Qty Approved", "The total quantity of this item in requests with status approved, fulfilled, or returned"), - ColumnDef("Qty Fulfilled", "The total quantity of this item in requests with status fulfilled or returned"), - ColumnDef("Qty Not Fulfilled", "The total quantity of this item that was requested but not given out, indicated by a request status of abandoned, cancelled, or declined"), - ColumnDef("Qty Returned", "The total quantity of this item in requests with status returned"), - ColumnDef("Qty Lost/Damaged", "The total quantity of this item in requests with status lost or damaged. The value in parentheses is this number divided by the item's Total Available value.") - ]; - - return ( - <> -
- - About this report - - - For accurate information, ensure that each item's Total Available value - is correct. - - - - - - - - - - {columnDefs.map((elem) => - - - )} - -
-
-
-
- -
- - } - /> - - ); + const { data, loading, error } = useQuery(DETAILED_ITEM_STATISTICS, { + partialRefetch: true, + }); + + const columns = useMemo( + () => [ + { + name: "Location", + selector: "item.location.location_name", + sortable: true, + grow: 2, + }, + { + name: "Item", + selector: "item.item_name", + sortable: true, + grow: 2, + }, + { + name: "Total Available", + selector: "item.totalAvailable", + sortable: true, + center: true, + }, + { + name: "Demand", + selector: "totalDemand", + sortable: true, + center: true, + }, + { + name: "Qty Approved", + selector: "totalApproved", + sortable: true, + center: true, + }, + { + name: "Qty Fulfilled", + selector: "totalFulfilled", + sortable: true, + center: true, + }, + { + name: "Qty Not Fulfilled", + selector: "totalNotFulfilled", + sortable: true, + center: true, + }, + { + name: "Qty Returned", + selector: "totalReturned", + sortable: true, + center: true, + }, + { + name: "Qty Lost/Damaged", + selector: "totalLostDamaged", + sortable: true, + center: true, + }, + ], + [] + ); + + const totalRequests = loading + ? 0 + : data.itemStatistics.reduce( + (prev: number, item: ItemWithStatistics) => prev + item.detailedQuantities.total, + 0 + ); + + function sumQtys(itemData: ItemWithStatistics, statuses: string[]) { + return statuses.reduce( + (accumulator: number, status: string) => itemData.detailedQuantities[status] + accumulator, + 0 + ); + } + + function calculatePartial( + itemData: ItemWithStatistics, + statuses: string[], + totalAvailable: number + ) { + const itemTotal = sumQtys(itemData, statuses); + const percent = ((itemTotal / totalAvailable) * 100).toPrecision(3); + + return `${itemTotal} (${percent}%)`; + } + + const calculatedData = + loading || totalRequests === 0 + ? [] + : data.itemStatistics.map((itemData: ItemWithStatistics) => { + const { totalAvailable } = itemData.item; + return { + ...itemData, + percentAllRequests: `${(itemData.detailedQuantities.total / totalRequests) * 100}%`, + totalDemand: `${itemData.detailedQuantities.total} (${( + (itemData.detailedQuantities.total / itemData.item.totalAvailable) * + 100 + ).toPrecision(3)}%)`, + totalApproved: calculatePartial( + itemData, + [APPROVED, FULFILLED, RETURNED], + totalAvailable + ), + totalFulfilled: calculatePartial(itemData, [FULFILLED, RETURNED], totalAvailable), + totalNotFulfilled: sumQtys(itemData, [ABANDONED, CANCELLED, DENIED]), + totalReturned: sumQtys(itemData, [RETURNED]), + totalLostDamaged: calculatePartial(itemData, [LOST, DAMAGED], totalAvailable), + }; + }); + + if (error) { + return ( + <> +
+ + Error displaying report +

Something is preventing us from showing this report: {error.message}

+
+ + ); + } + + const columnDefs = [ + ColumnDef("Demand", "The total quantity of this item that has ever been requested"), + ColumnDef( + "Qty Approved", + "The total quantity of this item in requests with status approved, fulfilled, or returned" + ), + ColumnDef( + "Qty Fulfilled", + "The total quantity of this item in requests with status fulfilled or returned" + ), + ColumnDef( + "Qty Not Fulfilled", + "The total quantity of this item that was requested but not given out, indicated by a request status of abandoned, cancelled, or declined" + ), + ColumnDef("Qty Returned", "The total quantity of this item in requests with status returned"), + ColumnDef( + "Qty Lost/Damaged", + "The total quantity of this item in requests with status lost or damaged. The value in parentheses is this number divided by the item's Total Available value." + ), + ]; + + return ( + <> +
+ + About this report + + + + For accurate information, ensure that each item's Total Available value is correct. + + + + + + + + + + + {columnDefs.map(elem => ( + + + + + ))} + +
+
+
+
+
+ + } + /> + + ); } export default ItemDemandReport; diff --git a/client/src/components/reports/statistics/DetailedItemStatistics.tsx b/client/src/components/reports/statistics/DetailedItemStatistics.tsx index c8a3c2a..980d480 100644 --- a/client/src/components/reports/statistics/DetailedItemStatistics.tsx +++ b/client/src/components/reports/statistics/DetailedItemStatistics.tsx @@ -1,131 +1,137 @@ -import React, {useMemo} from 'react'; +import React, { useMemo } from "react"; import DataTable from "react-data-table-component"; -import LoadingSpinner from "../../util/LoadingSpinner"; -import {useQuery} from "@apollo/client"; -import {DETAILED_ITEM_STATISTICS} from "../../util/graphql/Queries"; -import {Header, Icon, Message} from "semantic-ui-react"; +import { useQuery } from "@apollo/client"; +import { Header, Icon, Message } from "semantic-ui-react"; +import LoadingSpinner from "../../util/LoadingSpinner"; +import { DETAILED_ITEM_STATISTICS } from "../../util/graphql/Queries"; function DetailedItemStatistics(props: {}) { - const {data, loading, error} = useQuery(DETAILED_ITEM_STATISTICS, { - partialRefetch: true, - pollInterval: 120000 - }); - - const columns = useMemo(() => [ - { - name: "Location", - selector: "item.location.location_name", - sortable: true, - grow: 5 - - }, - { - name: "Item", - selector: "item.item_name", - sortable: true, - grow: 6 - }, - { - name: "Total", - selector: "detailedQuantities.total", - sortable: true, - center: true - }, - { - name: "Submitted", - selector: "detailedQuantities.SUBMITTED", - sortable: true, - center: true + const { data, loading, error } = useQuery(DETAILED_ITEM_STATISTICS, { + partialRefetch: true, + pollInterval: 120000, + }); - }, - { - name: "Approved", - selector: "detailedQuantities.APPROVED", - sortable: true, - center: true - }, - { - name: "Denied", - selector: "detailedQuantities.DENIED", - sortable: true, - center: true - }, - { - name: "RFP", - selector: "detailedQuantities.READY_FOR_PICKUP", - sortable: true, - center: true - }, - { - name: "Fulfilled", - selector: "detailedQuantities.FULFILLED", - sortable: true, - center: true - }, - { - name: "Returned", - selector: "detailedQuantities.RETURNED", - sortable: true, - center: true - }, - { - name: "Lost", - selector: "detailedQuantities.LOST", - sortable: true, - center: true - }, - { - name: "Damaged", - selector: "detailedQuantities.DAMAGED", - sortable: true, - center: true - }, - { - name: "Abandoned", - selector: "detailedQuantities.ABANDONED", - sortable: true, - center: true - }, - { - name: "Cancelled", - selector: "detailedQuantities.CANCELLED", - sortable: true, - center: true - }, - ], []); + const columns = useMemo( + () => [ + { + name: "Location", + selector: "item.location.location_name", + sortable: true, + grow: 5, + }, + { + name: "Item", + selector: "item.item_name", + sortable: true, + grow: 6, + }, + { + name: "Total", + selector: "detailedQuantities.total", + sortable: true, + center: true, + }, + { + name: "Submitted", + selector: "detailedQuantities.SUBMITTED", + sortable: true, + center: true, + }, + { + name: "Approved", + selector: "detailedQuantities.APPROVED", + sortable: true, + center: true, + }, + { + name: "Denied", + selector: "detailedQuantities.DENIED", + sortable: true, + center: true, + }, + { + name: "RFP", + selector: "detailedQuantities.READY_FOR_PICKUP", + sortable: true, + center: true, + }, + { + name: "Fulfilled", + selector: "detailedQuantities.FULFILLED", + sortable: true, + center: true, + }, + { + name: "Returned", + selector: "detailedQuantities.RETURNED", + sortable: true, + center: true, + }, + { + name: "Lost", + selector: "detailedQuantities.LOST", + sortable: true, + center: true, + }, + { + name: "Damaged", + selector: "detailedQuantities.DAMAGED", + sortable: true, + center: true, + }, + { + name: "Abandoned", + selector: "detailedQuantities.ABANDONED", + sortable: true, + center: true, + }, + { + name: "Cancelled", + selector: "detailedQuantities.CANCELLED", + sortable: true, + center: true, + }, + ], + [] + ); - if (error) { - return <> -
- - Error displaying report -

Something is preventing us from showing this report: {error.message}

-
- ; - } + if (error) { + return ( + <> +
+ + Error displaying report +

Something is preventing us from showing this report: {error.message}

+
+ + ); + } - return ( - <> -
- Data refreshes automatically every 2 minutes. - } - /> - - ); + return ( + <> +
+ + Data refreshes automatically every 2 minutes. + + } + /> + + ); } export default DetailedItemStatistics; diff --git a/client/src/components/requests/RequestedList.tsx b/client/src/components/requests/RequestedList.tsx index 9908bcf..35569f8 100644 --- a/client/src/components/requests/RequestedList.tsx +++ b/client/src/components/requests/RequestedList.tsx @@ -1,165 +1,168 @@ import React from "react"; -import {Card, Container, Header, Icon, Label, Loader, Message, Step} from "semantic-ui-react"; +import { Card, Container, Header, Icon, Label, Loader, Message, Step } from "semantic-ui-react"; +import { useQuery } from "@apollo/client"; + import { - ABANDONED, - APPROVED, - CANCELLED, - DAMAGED, - DENIED, - FULFILLED, - LOST, - READY_FOR_PICKUP, - RETURNED, - SUBMITTED + ABANDONED, + APPROVED, + CANCELLED, + DAMAGED, + DENIED, + FULFILLED, + LOST, + READY_FOR_PICKUP, + RETURNED, + SUBMITTED, } from "../../types/Hardware"; -import {useQuery} from "@apollo/client"; -import {USER_REQUESTS} from "../util/graphql/Queries"; -import {Request} from "../../types/Request"; +import { USER_REQUESTS } from "../util/graphql/Queries"; +import { Request } from "../../types/Request"; import ItemAndQuantity from "../desk/ItemAndQuantity"; -import {User} from "../../types/User"; +import { User } from "../../types/User"; interface RequestedListProps { - user: User + user: User; } -function RequestedList({user}: RequestedListProps) { - const {loading, error, data} = useQuery(USER_REQUESTS, { - variables: { - uuid: user.uuid - }, - pollInterval: 30000 - }); - if (loading) { - return ( - - ); - } +function RequestedList({ user }: RequestedListProps) { + const { loading, error, data } = useQuery(USER_REQUESTS, { + variables: { + uuid: user.uuid, + }, + pollInterval: 30000, + }); + if (loading) { + return ; + } - if (error) { - return ( - - - {error.message} - ); - } + if (error) { + return ( + + + {error.message} + + ); + } + + let steps = ( + + + + Loading steps + + + + ); - let steps = ( - - - - Loading steps - - - + if (data.requests.length === 0) { + return ( + +
+ You haven't requested any hardware yet. To request an item, select the quantity and click + on the blue Request button. +
+
); + } - if (data.requests.length === 0) { - return ( - -
- You haven't requested any hardware yet. To request an item, - select the quantity and click on the blue Request button. -
-
- ); - } + if (data.requests.length > 0) { + return data.requests + .sort( + (a: Request, b: Request) => + a.item.location.location_name.localeCompare(b.item.location.location_name) || + a.item.item_name.localeCompare(b.item.item_name) || + a.request_id - b.request_id + ) + .map((r: Request) => { + const returnInfo = r.item.returnRequired && + r.status !== RETURNED && + r.status !== DENIED && + r.status !== CANCELLED && + r.status !== ABANDONED && ( + + ); - if (data.requests.length > 0) { - return data.requests.sort((a: Request, b: Request) => a.item.location.location_name.localeCompare(b.item.location.location_name) || a.item.item_name.localeCompare(b.item.item_name) || a.request_id - b.request_id).map((r: Request) => { + const returned = r.status === RETURNED && ( + + ); - let returnInfo = (r.item.returnRequired && r.status !== RETURNED && r.status !== DENIED - && r.status !== CANCELLED && r.status !== ABANDONED) && ( - - ) + const locationInfo = ( + + + + ); - let returned = (r.status === RETURNED) && ( -
= 3} index={index} > diff --git a/client/src/components/inventory/RequestButton.tsx b/client/src/components/inventory/RequestButton.tsx index a2df4d7..627d6cb 100644 --- a/client/src/components/inventory/RequestButton.tsx +++ b/client/src/components/inventory/RequestButton.tsx @@ -9,13 +9,13 @@ import { RequestedItem } from "../../types/Hardware"; import { GET_SETTING, USER_REQUESTS } from "../util/graphql/Queries"; import { User } from "../../types/User"; -interface RequestButtonProps { +interface Props { requestedItem: RequestedItem; user: User; toastManager: any; } -function RequestButton({ requestedItem, user, toastManager }: RequestButtonProps) { +const RequestButton: React.FC = ({ requestedItem, user, toastManager }) => { const [createRequest, { loading, error }] = useMutation(CREATE_REQUEST, { refetchQueries: [ { @@ -40,23 +40,23 @@ function RequestButton({ requestedItem, user, toastManager }: RequestButtonProps /> ); } - let requests_allowed = "true"; + let requestsAllowed = "true"; return ( - {({ loading, error, data }: any) => { - if (loading) { + {({ loading: queryLoading, error: queryError, data }: any) => { + if (queryLoading) { return ; } - if (!error && data.setting !== undefined) { - requests_allowed = data.setting.value; + if (!queryError && data.setting !== undefined) { + requestsAllowed = data.setting.value; } return (