From a689613a4572be31dc160cccc71751892dda2b0d Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Sun, 8 Nov 2020 08:23:39 +0000 Subject: [PATCH] Tag management window (#24) Add a window to manage tags. --- client/package-lock.json | 1219 ++++++++--------- client/package.json | 6 +- client/src/api.ts | 16 + client/src/components/MainWindow.tsx | 20 +- client/src/components/appbar/AddTabMenu.tsx | 8 + .../common/DiscardChangesButton.tsx | 13 + client/src/components/common/MenuEditText.tsx | 23 + .../querybuilder/QBSelectWithRequest.tsx | 12 - client/src/components/windows/Windows.tsx | 21 +- .../windows/{ => album}/AlbumWindow.tsx | 94 +- .../windows/{ => artist}/ArtistWindow.tsx | 94 +- .../windows/manage_tags/ManageTagMenu.tsx | 97 ++ .../windows/manage_tags/ManageTagsWindow.tsx | 479 +++++++ .../windows/manage_tags/NewTagMenu.tsx | 54 + .../windows/manage_tags/TagChange.tsx | 147 ++ .../windows/{ => query}/QueryWindow.tsx | 112 +- .../windows/{ => song}/SongWindow.tsx | 57 +- .../windows/{ => tag}/TagWindow.tsx | 115 +- .../Getters.tsx => backend/queries.tsx} | 64 +- client/src/lib/backend/tags.tsx | 61 + client/src/lib/query/Query.tsx | 14 +- server/app.ts | 4 + server/endpoints/DeleteTagEndpointHandler.ts | 76 + server/endpoints/MergeTagEndpointHandler.ts | 73 + server/endpoints/QueryEndpointHandler.ts | 31 +- 25 files changed, 1896 insertions(+), 1014 deletions(-) create mode 100644 client/src/components/common/DiscardChangesButton.tsx create mode 100644 client/src/components/common/MenuEditText.tsx rename client/src/components/windows/{ => album}/AlbumWindow.tsx (71%) rename client/src/components/windows/{ => artist}/ArtistWindow.tsx (71%) create mode 100644 client/src/components/windows/manage_tags/ManageTagMenu.tsx create mode 100644 client/src/components/windows/manage_tags/ManageTagsWindow.tsx create mode 100644 client/src/components/windows/manage_tags/NewTagMenu.tsx create mode 100644 client/src/components/windows/manage_tags/TagChange.tsx rename client/src/components/windows/{ => query}/QueryWindow.tsx (59%) rename client/src/components/windows/{ => song}/SongWindow.tsx (80%) rename client/src/components/windows/{ => tag}/TagWindow.tsx (70%) rename client/src/lib/{query/Getters.tsx => backend/queries.tsx} (67%) create mode 100644 client/src/lib/backend/tags.tsx create mode 100644 server/endpoints/DeleteTagEndpointHandler.ts create mode 100644 server/endpoints/MergeTagEndpointHandler.ts diff --git a/client/package-lock.json b/client/package-lock.json index 5fbb9c8..b8c35fe 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -65,13 +65,20 @@ } }, "@babel/generator": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.5.tgz", - "integrity": "sha512-9UqHWJ4IwRTy4l0o8gq2ef8ws8UPzvtMkVKjTLAiRmza9p9V6Z+OfuNd9fB1j5Q67F+dVJtPC2sZXI8NM9br4g==", + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", "requires": { "@babel/types": "^7.11.5", "jsesc": "^2.5.1", - "source-map": "^0.6.1" + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } } }, "@babel/helper-annotate-as-pure": { @@ -1761,9 +1768,9 @@ "integrity": "sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA==" }, "@types/babel__core": { - "version": "7.1.9", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.9.tgz", - "integrity": "sha512-sY2RsIJ5rpER1u3/aQ8OFSI7qGIy8o1NEEbgb2UaJcvOtXOMpd39ko723NBpjQFg9SIX7TXtjejZVGeIMLhoOw==", + "version": "7.1.10", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.10.tgz", + "integrity": "sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw==", "requires": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0", @@ -1773,26 +1780,26 @@ } }, "@types/babel__generator": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", - "integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", + "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", "requires": { "@babel/types": "^7.0.0" } }, "@types/babel__template": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", - "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.3.tgz", + "integrity": "sha512-uCoznIPDmnickEi6D0v11SBpW0OuVqHJCa7syXqQHy5uktSCreIlt0iglsCnmvz8yCb38hGcWeseA8cWJSwv5Q==", "requires": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "@types/babel__traverse": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.13.tgz", - "integrity": "sha512-i+zS7t6/s9cdQvbqKDARrcbrPvtJGlbYsMkazo03nTAK3RX9FNrLllXys22uiTGJapPOTZTQ35nHh4ISph4SLQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.15.tgz", + "integrity": "sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==", "requires": { "@babel/types": "^7.3.0" } @@ -2059,6 +2066,11 @@ } } }, + "@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" + }, "@types/yargs": { "version": "13.0.10", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.10.tgz", @@ -2294,6 +2306,11 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -2303,10 +2320,31 @@ "negotiator": "0.6.2" } }, + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==" + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" + } + } + }, "acorn-jsx": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", - "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==" + "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==" }, "acorn-walk": { "version": "6.2.0", @@ -2370,9 +2408,9 @@ } }, "ajv": { - "version": "6.12.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", - "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "version": "6.12.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", + "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2509,6 +2547,26 @@ "define-properties": "^1.1.3", "es-abstract": "^1.17.0", "is-string": "^1.0.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "array-union": { @@ -2536,6 +2594,26 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "arrify": { @@ -3355,6 +3433,13 @@ "parse-asn1": "^5.1.5", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "browserify-zlib": { @@ -3366,14 +3451,14 @@ } }, "browserslist": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.0.tgz", - "integrity": "sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ==", + "version": "4.14.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.5.tgz", + "integrity": "sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==", "requires": { - "caniuse-lite": "^1.0.30001111", - "electron-to-chromium": "^1.3.523", - "escalade": "^3.0.2", - "node-releases": "^1.1.60" + "caniuse-lite": "^1.0.30001135", + "electron-to-chromium": "^1.3.571", + "escalade": "^3.1.0", + "node-releases": "^1.1.61" } }, "bser": { @@ -3534,9 +3619,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001122", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001122.tgz", - "integrity": "sha512-pxjw28CThdrqfz06nJkpAc5SXM404TXB/h5f4UJX+rrXJKE/1bu/KAILc2AY+O6cQIFtRjV9qOR2vaEp9LDGUA==" + "version": "1.0.30001140", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001140.tgz", + "integrity": "sha512-xFtvBtfGrpjTOxTpjP5F2LmN04/ZGfYV8EQzUIC/RmKpdrmzJrjqlJ4ho7sGuAMPko2/Jl08h7x9uObCfBFaAA==" }, "canvg": { "version": "3.0.6", @@ -3788,11 +3873,6 @@ "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=" - }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -3908,11 +3988,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -3951,11 +4026,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3997,13 +4067,6 @@ "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", "requires": { "safe-buffer": "5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } } }, "content-type": { @@ -4017,13 +4080,6 @@ "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", "requires": { "safe-buffer": "~5.1.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } } }, "cookie": { @@ -4438,6 +4494,14 @@ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" }, + "cssstyle": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "requires": { + "cssom": "0.3.x" + } + }, "csstype": { "version": "2.6.13", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.13.tgz", @@ -4480,24 +4544,6 @@ "whatwg-url": "^7.0.0" }, "dependencies": { - "abab": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", - "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==" - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, "whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", @@ -4521,11 +4567,11 @@ "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==" }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "decamelize": { @@ -4828,9 +4874,9 @@ }, "dependencies": { "domelementtype": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", - "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz", + "integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==" } } }, @@ -4850,13 +4896,6 @@ "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", "requires": { "webidl-conversions": "^4.0.2" - }, - "dependencies": { - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - } } }, "domhandler": { @@ -4892,9 +4931,9 @@ } }, "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "requires": { "is-obj": "^2.0.0" } @@ -4944,11 +4983,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -4974,9 +5008,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.556", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.556.tgz", - "integrity": "sha512-g5cGpg6rOCXxyfaLCQIWz9Fx+raFfbZ6sc4QLfvvaiCERBzY6YD6rh5d12QN++bEF1Tm9osYnxP37lbN/92j4A==" + "version": "1.3.576", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.576.tgz", + "integrity": "sha512-uSEI0XZ//5ic+0NdOqlxp0liCD44ck20OAGyLMSymIWTEAtHKVJi6JM18acOnRgUgX7Q65QqnI+sNncNvIy8ew==" }, "elliptic": { "version": "6.5.3", @@ -5060,11 +5094,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -5097,17 +5126,18 @@ } }, "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "version": "1.18.0-next.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", + "integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1", "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", "object-keys": "^1.1.1", "object.assign": "^4.1.0", "string.prototype.trimend": "^1.0.1", @@ -5154,9 +5184,9 @@ } }, "escalade": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz", - "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz", + "integrity": "sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==" }, "escape-html": { "version": "1.0.3", @@ -5595,11 +5625,11 @@ "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==" }, "eslint-scope": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", - "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "requires": { - "esrecurse": "^4.1.0", + "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, @@ -5624,13 +5654,6 @@ "acorn": "^7.1.1", "acorn-jsx": "^5.2.0", "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "acorn": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", - "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==" - } } }, "esprima": { @@ -5856,11 +5879,6 @@ "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -6203,11 +6221,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -6319,11 +6332,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -6382,11 +6390,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -6656,6 +6659,13 @@ "inherits": "^2.0.4", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "hash.js": { @@ -6743,11 +6753,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -6889,11 +6894,6 @@ } } }, - "http-parser-js": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", - "integrity": "sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==" - }, "http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -7148,6 +7148,26 @@ "es-abstract": "^1.17.0-next.1", "has": "^1.0.3", "side-channel": "^1.0.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "invariant": { @@ -7158,11 +7178,6 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -7215,9 +7230,9 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-callable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", - "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" }, "is-ci": { "version": "2.0.0", @@ -7615,108 +7630,6 @@ "jest-mock": "^24.9.0", "jest-util": "^24.9.0", "jsdom": "^11.5.1" - }, - "dependencies": { - "abab": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", - "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==" - }, - "acorn": { - "version": "5.7.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", - "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==" - }, - "acorn-globals": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", - "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" - } - } - }, - "cssstyle": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", - "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", - "requires": { - "cssom": "0.3.x" - } - }, - "jsdom": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", - "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", - "requires": { - "abab": "^2.0.0", - "acorn": "^5.5.3", - "acorn-globals": "^4.1.0", - "array-equal": "^1.0.0", - "cssom": ">= 0.3.2 < 0.4.0", - "cssstyle": "^1.0.0", - "data-urls": "^1.0.0", - "domexception": "^1.0.1", - "escodegen": "^1.9.1", - "html-encoding-sniffer": "^1.0.2", - "left-pad": "^1.3.0", - "nwsapi": "^2.0.7", - "parse5": "4.0.0", - "pn": "^1.1.0", - "request": "^2.87.0", - "request-promise-native": "^1.0.5", - "sax": "^1.2.4", - "symbol-tree": "^3.2.2", - "tough-cookie": "^2.3.4", - "w3c-hr-time": "^1.0.1", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.3", - "whatwg-mimetype": "^2.1.0", - "whatwg-url": "^6.4.1", - "ws": "^5.2.0", - "xml-name-validator": "^3.0.0" - } - }, - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==" - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" - } } }, "jest-environment-jsdom-fourteen": { @@ -7732,33 +7645,11 @@ "jsdom": "^14.1.0" }, "dependencies": { - "abab": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", - "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==" - }, "acorn": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" }, - "acorn-globals": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", - "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - } - }, - "cssstyle": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", - "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", - "requires": { - "cssom": "0.3.x" - } - }, "jsdom": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-14.1.0.tgz", @@ -7797,19 +7688,6 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, "whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", @@ -7827,11 +7705,6 @@ "requires": { "async-limiter": "~1.0.0" } - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" } } }, @@ -8186,6 +8059,46 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, + "jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "requires": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==" + } + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -8197,9 +8110,9 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, "json-parse-even-better-errors": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.0.tgz", - "integrity": "sha512-o3aP+RsWDJZayj1SbHNQAI8x0v3T3SKiGoZlNYfbUP1S3omJQ6i9CnqADqkSPaOAxwua4/1YWx5CM7oiChJt2Q==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "json-schema": { "version": "0.2.3", @@ -8415,14 +8328,6 @@ "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "requires": { - "invert-kv": "^2.0.0" - } - }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", @@ -8658,14 +8563,6 @@ "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==" }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -8723,16 +8620,6 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memoize-one": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", @@ -8766,11 +8653,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -9131,9 +9013,9 @@ } }, "node-forge": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", - "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==" + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" }, "node-int64": { "version": "0.4.0", @@ -9204,11 +9086,6 @@ } } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", @@ -9251,9 +9128,9 @@ } }, "node-releases": { - "version": "1.1.60", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz", - "integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==" + "version": "1.1.61", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.61.tgz", + "integrity": "sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==" }, "normalize-package-data": { "version": "2.5.0", @@ -9318,11 +9195,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", @@ -9375,6 +9247,26 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "object-keys": { @@ -9396,14 +9288,14 @@ } }, "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" } }, "object.entries": { @@ -9414,6 +9306,26 @@ "define-properties": "^1.1.3", "es-abstract": "^1.17.5", "has": "^1.0.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "object.fromentries": { @@ -9425,6 +9337,26 @@ "es-abstract": "^1.17.0-next.1", "function-bind": "^1.1.1", "has": "^1.0.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "object.getownpropertydescriptors": { @@ -9434,6 +9366,26 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "object.pick": { @@ -9453,6 +9405,26 @@ "es-abstract": "^1.17.0-next.1", "function-bind": "^1.1.1", "has": "^1.0.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "obuf": { @@ -9490,9 +9462,9 @@ } }, "open": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/open/-/open-7.2.1.tgz", - "integrity": "sha512-xbYCJib4spUdmcs0g/2mK1nKo/jO2T7INClWd/beL7PFkXRWgr8B23ssDHX/USPn2M2IjDR5UdpYs6I67SnTSA==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.3.0.tgz", + "integrity": "sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw==", "requires": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -9551,26 +9523,11 @@ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" - }, "p-each-series": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", @@ -9584,11 +9541,6 @@ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" - }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -9665,11 +9617,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -9725,6 +9672,11 @@ "json-parse-better-errors": "^1.0.1" } }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -9903,9 +9855,9 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, "postcss": { - "version": "7.0.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", "requires": { "chalk": "^2.4.2", "source-map": "^0.6.1", @@ -10219,9 +10171,9 @@ } }, "postcss-load-config": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.0.tgz", - "integrity": "sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.2.tgz", + "integrity": "sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==", "requires": { "cosmiconfig": "^5.0.0", "import-cwd": "^2.0.0" @@ -10770,13 +10722,14 @@ } }, "postcss-selector-parser": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", - "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", "requires": { "cssesc": "^3.0.0", "indexes-of": "^1.0.1", - "uniq": "^1.0.1" + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" } }, "postcss-svgo": { @@ -11387,9 +11340,9 @@ } }, "react-scripts": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.1.tgz", - "integrity": "sha512-JpTdi/0Sfd31mZA6Ukx+lq5j1JoKItX7qqEK4OiACjVQletM1P38g49d9/D0yTxp9FrSF+xpJFStkGgKEIRjlQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.3.tgz", + "integrity": "sha512-oSnoWmii/iKdeQiwaO6map1lUaZLmG0xIUyb/HwCVFLT7gNbj8JZ9RmpvMCZ4fB98ZUMRfNmp/ft8uy/xD1RLA==", "requires": { "@babel/core": "7.9.0", "@svgr/webpack": "4.3.3", @@ -11437,11 +11390,11 @@ "sass-loader": "8.0.2", "semver": "6.3.0", "style-loader": "0.23.1", - "terser-webpack-plugin": "2.3.5", + "terser-webpack-plugin": "2.3.8", "ts-pnp": "1.1.6", "url-loader": "2.3.0", "webpack": "4.42.0", - "webpack-dev-server": "3.10.3", + "webpack-dev-server": "3.11.0", "webpack-manifest-plugin": "2.2.0", "workbox-webpack-plugin": "4.3.1" } @@ -11575,6 +11528,26 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "regexpp": { @@ -11583,9 +11556,9 @@ "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==" }, "regexpu-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", - "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", "requires": { "regenerate": "^1.4.0", "regenerate-unicode-properties": "^8.2.0", @@ -11712,6 +11685,13 @@ "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } } }, "request-promise-core": { @@ -11938,17 +11918,17 @@ } }, "rxjs": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", - "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "requires": { "tslib": "^1.9.0" } }, "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -12059,11 +12039,11 @@ "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" }, "selfsigned": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", - "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz", + "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==", "requires": { - "node-forge": "0.9.0" + "node-forge": "^0.10.0" } }, "semver": { @@ -12119,9 +12099,12 @@ } }, "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "requires": { + "randombytes": "^2.1.0" + } }, "serve-index": { "version": "1.9.1", @@ -12285,27 +12268,6 @@ "requires": { "es-abstract": "^1.18.0-next.0", "object-inspect": "^1.8.0" - }, - "dependencies": { - "es-abstract": { - "version": "1.18.0-next.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", - "integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } } }, "signal-exit": { @@ -12466,12 +12428,20 @@ } }, "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", + "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", "requires": { "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" + "uuid": "^3.4.0", + "websocket-driver": "0.6.5" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } } }, "sockjs-client": { @@ -12573,9 +12543,9 @@ } }, "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", + "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==" }, "spdy": { "version": "4.0.2", @@ -12713,11 +12683,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -12768,11 +12733,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -12853,6 +12813,26 @@ "internal-slot": "^1.0.2", "regexp.prototype.flags": "^1.3.0", "side-channel": "^1.0.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "string.prototype.trimend": { @@ -12862,6 +12842,26 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "string.prototype.trimstart": { @@ -12871,6 +12871,26 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "string_decoder": { @@ -12879,6 +12899,13 @@ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "requires": { "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "stringify-object": { @@ -13073,18 +13100,18 @@ } }, "terser-webpack-plugin": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.5.tgz", - "integrity": "sha512-WlWksUoq+E4+JlJ+h+U+QUzXpcsMSSNXkDy9lBVkSqDn1w23Gg29L/ary9GeJVYCGiNJJX7LnVc4bwL1N3/g1w==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz", + "integrity": "sha512-/fKw3R+hWyHfYx7Bv6oPqmk4HGQcrWLtV3X6ggvPuwPNHSnzvVV51z6OaaCOus4YLjutYGOz3pEpbhe6Up2s1w==", "requires": { "cacache": "^13.0.1", - "find-cache-dir": "^3.2.0", - "jest-worker": "^25.1.0", - "p-limit": "^2.2.2", - "schema-utils": "^2.6.4", - "serialize-javascript": "^2.1.2", + "find-cache-dir": "^3.3.1", + "jest-worker": "^25.4.0", + "p-limit": "^2.3.0", + "schema-utils": "^2.6.6", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", - "terser": "^4.4.3", + "terser": "^4.6.12", "webpack-sources": "^1.4.3" }, "dependencies": { @@ -13222,11 +13249,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -13330,6 +13352,14 @@ "punycode": "^2.1.1" } }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "requires": { + "punycode": "^2.1.0" + } + }, "ts-pnp": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.1.6.tgz", @@ -13612,6 +13642,26 @@ "es-abstract": "^1.17.2", "has-symbols": "^1.0.1", "object.getownpropertydescriptors": "^2.1.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "utila": { @@ -13625,9 +13675,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" }, "v8-compile-cache": { "version": "2.1.1", @@ -13689,18 +13739,6 @@ "domexception": "^1.0.1", "webidl-conversions": "^4.0.2", "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" - } } }, "wait-for-expect": { @@ -13836,12 +13874,6 @@ "readable-stream": "^2.0.2" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -13861,6 +13893,11 @@ "minimalistic-assert": "^1.0.0" } }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, "webpack": { "version": "4.42.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.0.tgz", @@ -13937,14 +13974,6 @@ "ajv-keywords": "^3.1.0" } }, - "serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "requires": { - "randombytes": "^2.1.0" - } - }, "ssri": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", @@ -13984,9 +14013,9 @@ } }, "webpack-dev-server": { - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz", - "integrity": "sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", + "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", "requires": { "ansi-html": "0.0.7", "bonjour": "^3.5.0", @@ -13996,31 +14025,31 @@ "debug": "^4.1.1", "del": "^4.1.1", "express": "^4.17.1", - "html-entities": "^1.2.1", + "html-entities": "^1.3.1", "http-proxy-middleware": "0.19.1", "import-local": "^2.0.0", "internal-ip": "^4.3.0", "ip": "^1.1.5", "is-absolute-url": "^3.0.3", "killable": "^1.0.1", - "loglevel": "^1.6.6", + "loglevel": "^1.6.8", "opn": "^5.5.0", "p-retry": "^3.0.1", - "portfinder": "^1.0.25", + "portfinder": "^1.0.26", "schema-utils": "^1.0.0", "selfsigned": "^1.10.7", "semver": "^6.3.0", "serve-index": "^1.9.1", - "sockjs": "0.3.19", + "sockjs": "0.3.20", "sockjs-client": "1.4.0", - "spdy": "^4.0.1", + "spdy": "^4.0.2", "strip-ansi": "^3.0.1", "supports-color": "^6.1.0", "url": "^0.11.0", "webpack-dev-middleware": "^3.7.2", "webpack-log": "^2.0.0", "ws": "^6.2.1", - "yargs": "12.0.5" + "yargs": "^13.3.2" }, "dependencies": { "ansi-regex": { @@ -14052,42 +14081,12 @@ "upath": "^1.1.1" } }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "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" - } - } - } - }, "fsevents": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "optional": true }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" - }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -14120,11 +14119,6 @@ "binary-extensions": "^1.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=" - }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -14159,16 +14153,6 @@ "readable-stream": "^2.0.2" } }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -14179,30 +14163,6 @@ "ajv-keywords": "^3.1.0" } }, - "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" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "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" - } - } - } - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -14227,35 +14187,6 @@ "has-flag": "^3.0.0" } }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "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" - } - } - } - }, "ws": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", @@ -14263,34 +14194,6 @@ "requires": { "async-limiter": "~1.0.0" } - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -14301,6 +14204,13 @@ "requires": { "ansi-colors": "^3.0.0", "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } } }, "webpack-manifest-plugin": { @@ -14336,12 +14246,10 @@ } }, "websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", "requires": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, @@ -14359,15 +14267,25 @@ } }, "whatwg-fetch": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz", - "integrity": "sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ==" + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz", + "integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==" }, "whatwg-mimetype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -14626,6 +14544,11 @@ "async-limiter": "~1.0.0" } }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/client/package.json b/client/package.json index fec4ea1..1fb3ac0 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "@types/react-dom": "^16.9.0", "@types/react-router": "^5.1.8", "@types/react-router-dom": "^5.1.5", + "@types/uuid": "^8.3.0", "jsurl": "^0.1.5", "lodash": "^4.17.20", "material-table": "^1.69.0", @@ -24,8 +25,9 @@ "react-dnd-html5-backend": "^11.1.3", "react-dom": "^16.13.1", "react-router-dom": "^5.2.0", - "react-scripts": "3.4.1", - "typescript": "~3.7.2" + "react-scripts": "^3.4.3", + "typescript": "~3.7.2", + "uuid": "^8.3.0" }, "scripts": { "dev": "BROWSER=none react-scripts start", diff --git a/client/src/api.ts b/client/src/api.ts index b671e3e..7811d5d 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -302,4 +302,20 @@ export interface TagDetailsResponse { } export function checkTagDetailsRequest(req: any): boolean { return true; +} + +// Delete tag (DELETE). +export const DeleteTagEndpoint = '/tag/:id'; +export interface DeleteTagRequest { } +export interface DeleteTagResponse { } +export function checkDeleteTagRequest(req: any): boolean { + return true; +} + +// Merge tag (POST). +export const MergeTagEndpoint = '/tag/:id/merge/:toId'; +export interface MergeTagRequest { } +export interface MergeTagResponse { } +export function checkMergeTagRequest(req: any): boolean { + return true; } \ No newline at end of file diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index d3b73f1..d600867 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -2,13 +2,14 @@ import React, { useReducer, Reducer } from 'react'; import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; import { grey } from '@material-ui/core/colors'; import AppBar from './appbar/AppBar'; -import QueryWindow from './windows/QueryWindow'; +import QueryWindow from './windows/query/QueryWindow'; import { NewTabProps } from './appbar/AddTabMenu'; import { newWindowState, newWindowReducer, WindowType } from './windows/Windows'; -import ArtistWindow from './windows/ArtistWindow'; -import AlbumWindow from './windows/AlbumWindow'; -import TagWindow from './windows/TagWindow'; -import SongWindow from './windows/SongWindow'; +import ArtistWindow from './windows/artist/ArtistWindow'; +import AlbumWindow from './windows/album/AlbumWindow'; +import TagWindow from './windows/tag/TagWindow'; +import SongWindow from './windows/song/SongWindow'; +import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow'; var _ = require('lodash'); const darkTheme = createMuiTheme({ @@ -76,6 +77,7 @@ export default function MainWindow(props: any) { newWindowState[WindowType.Album](), newWindowState[WindowType.Artist](), newWindowState[WindowType.Tag](), + newWindowState[WindowType.ManageTags](), ], tabReducers: [ newWindowReducer[WindowType.Query], @@ -83,6 +85,7 @@ export default function MainWindow(props: any) { newWindowReducer[WindowType.Album], newWindowReducer[WindowType.Artist], newWindowReducer[WindowType.Tag], + newWindowReducer[WindowType.ManageTags], ], tabTypes: [ WindowType.Query, @@ -90,6 +93,7 @@ export default function MainWindow(props: any) { WindowType.Album, WindowType.Artist, WindowType.Tag, + WindowType.ManageTags, ], activeTab: 0 }) @@ -134,6 +138,12 @@ export default function MainWindow(props: any) { dispatch={tabDispatch} mainDispatch={dispatch} /> + case WindowType.ManageTags: + return default: throw new Error("Unimplemented window type"); } diff --git a/client/src/components/appbar/AddTabMenu.tsx b/client/src/components/appbar/AddTabMenu.tsx index b001fe1..a9e0755 100644 --- a/client/src/components/appbar/AddTabMenu.tsx +++ b/client/src/components/appbar/AddTabMenu.tsx @@ -28,5 +28,13 @@ export default function AddTabMenu(props: IProps) { }) }} >{WindowType.Query} + { + props.onClose(); + props.onCreateTab({ + windowType: WindowType.ManageTags, + }) + }} + >Manage Tags } \ No newline at end of file diff --git a/client/src/components/common/DiscardChangesButton.tsx b/client/src/components/common/DiscardChangesButton.tsx new file mode 100644 index 0000000..0c6e958 --- /dev/null +++ b/client/src/components/common/DiscardChangesButton.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Box, Button } from '@material-ui/core'; + +export default function DiscardChangesButton(props: any) { + return + + +} \ No newline at end of file diff --git a/client/src/components/common/MenuEditText.tsx b/client/src/components/common/MenuEditText.tsx new file mode 100644 index 0000000..5939000 --- /dev/null +++ b/client/src/components/common/MenuEditText.tsx @@ -0,0 +1,23 @@ +import React, { useState } from 'react'; +import { TextField } from '@material-ui/core'; + +export default function MenuEditText(props: { + label: string, + onSubmit: (s: string) => void, +}) { + const [input, setInput] = useState(""); + + return setInput(e.target.value)} + onKeyDown={(e: any) => { + if (e.key === 'Enter') { + // User submitted free-form value. + props.onSubmit(input); + e.preventDefault(); + } + }} + /> +} \ No newline at end of file diff --git a/client/src/components/querybuilder/QBSelectWithRequest.tsx b/client/src/components/querybuilder/QBSelectWithRequest.tsx index dd1b115..107e982 100644 --- a/client/src/components/querybuilder/QBSelectWithRequest.tsx +++ b/client/src/components/querybuilder/QBSelectWithRequest.tsx @@ -43,18 +43,6 @@ export default function QBSelectWithRequest(props: IProps & any) { })(); }; - // // Ensure a new request is made whenever the loading option is enabled. - // useEffect(() => { - // startRequest(input); - // }, []); - - // Ensure options are cleared whenever the element is closed. - // useEffect(() => { - // if (!open) { - // setOptions(null); - // } - // }, [open]); - useEffect(() => { startRequest(input); }, [input]); diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx index d5840e5..1411e33 100644 --- a/client/src/components/windows/Windows.tsx +++ b/client/src/components/windows/Windows.tsx @@ -1,15 +1,17 @@ import React from 'react'; -import { QueryWindowReducer } from "./QueryWindow"; -import { ArtistWindowReducer } from "./ArtistWindow"; +import { QueryWindowReducer } from "./query/QueryWindow"; +import { ArtistWindowReducer } from "./artist/ArtistWindow"; import SearchIcon from '@material-ui/icons/Search'; import PersonIcon from '@material-ui/icons/Person'; import AlbumIcon from '@material-ui/icons/Album'; import LocalOfferIcon from '@material-ui/icons/LocalOffer'; import AudiotrackIcon from '@material-ui/icons/Audiotrack'; -import { SongWindowReducer } from './SongWindow'; -import { AlbumWindowReducer } from './AlbumWindow'; -import { TagWindowReducer } from './TagWindow'; +import LoyaltyIcon from '@material-ui/icons/Loyalty'; +import { SongWindowReducer } from './song/SongWindow'; +import { AlbumWindowReducer } from './album/AlbumWindow'; +import { TagWindowReducer } from './tag/TagWindow'; import { songGetters } from '../../lib/songGetters'; +import { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow'; export enum WindowType { Query = "Query", @@ -17,6 +19,7 @@ export enum WindowType { Album = "Album", Tag = "Tag", Song = "Song", + ManageTags = "ManageTags", } export interface WindowState { @@ -29,6 +32,7 @@ export const newWindowReducer = { [WindowType.Album]: AlbumWindowReducer, [WindowType.Song]: SongWindowReducer, [WindowType.Tag]: TagWindowReducer, + [WindowType.ManageTags]: ManageTagsWindowReducer, } export const newWindowState = { @@ -78,4 +82,11 @@ export const newWindowState = { songsWithTag: null, } }, + [WindowType.ManageTags]: () => { + return { + tabLabel: <>Manage Tags, + fetchedTags: null, + pendingChanges: [], + } + } } \ No newline at end of file diff --git a/client/src/components/windows/AlbumWindow.tsx b/client/src/components/windows/album/AlbumWindow.tsx similarity index 71% rename from client/src/components/windows/AlbumWindow.tsx rename to client/src/components/windows/album/AlbumWindow.tsx index 6cefcb8..d879bfc 100644 --- a/client/src/components/windows/AlbumWindow.tsx +++ b/client/src/components/windows/album/AlbumWindow.tsx @@ -1,13 +1,15 @@ import React, { useEffect, useState } from 'react'; import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core'; import AlbumIcon from '@material-ui/icons/Album'; -import * as serverApi from '../../api'; -import { WindowState } from './Windows'; -import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; -import EditableText from '../common/EditableText'; -import SubmitChangesButton from '../common/SubmitChangesButton'; -import SongTable, { SongGetters } from '../tables/ResultsTable'; -import { saveAlbumChanges } from '../../lib/saveChanges'; +import * as serverApi from '../../../api'; +import { WindowState } from '../Windows'; +import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; +import EditableText from '../../common/EditableText'; +import SubmitChangesButton from '../../common/SubmitChangesButton'; +import SongTable, { SongGetters } from '../../tables/ResultsTable'; +import { saveAlbumChanges } from '../../../lib/saveChanges'; +import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; +import { queryAlbums, querySongs } from '../../../lib/backend/queries'; var _ = require('lodash'); export type AlbumMetadata = serverApi.AlbumDetails; @@ -50,38 +52,15 @@ export interface IProps { } export async function getAlbumMetadata(id: number) { - const query = { - prop: serverApi.QueryElemProperty.albumId, - propOperand: id, - propOperator: serverApi.QueryFilterOp.Eq, - }; - - var q: serverApi.QueryRequest = { - query: query, - offsetsLimits: { - albumOffset: 0, - albumLimit: 1, - }, - ordering: { - orderBy: { - type: serverApi.OrderByType.Name, - }, - ascending: true, + return (await queryAlbums({ + query: { + a: QueryLeafBy.AlbumId, + b: id, + leafOp: QueryLeafOp.Equals, }, - }; - - const requestOpts = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(q), - }; - - return (async () => { - const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) - let json: any = await response.json(); - let album = json.albums[0]; - return album; - })(); + offset: 0, + limit: 1, + }))[0]; } export default function AlbumWindow(props: IProps) { @@ -103,37 +82,20 @@ export default function AlbumWindow(props: IProps) { useEffect(() => { if (props.state.songsOnAlbum) { return; } - var q: serverApi.QueryRequest = { - query: { - prop: serverApi.QueryElemProperty.albumId, - propOperator: serverApi.QueryFilterOp.Eq, - propOperand: props.state.albumId, - }, - offsetsLimits: { - songOffset: 0, - songLimit: 100, - }, - ordering: { - orderBy: { - type: serverApi.OrderByType.Name, - }, - ascending: true, - }, - }; - - const requestOpts = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(q), - }; - (async () => { - const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) - let json: any = await response.json(); + const songs = await querySongs({ + query: { + a: QueryLeafBy.AlbumId, + b: props.state.albumId, + leafOp: QueryLeafOp.Equals, + }, + offset: 0, + limit: -1, + }); props.dispatch({ type: AlbumWindowStateActions.SetSongs, - value: json.songs, - }); + value: songs, + }); })(); }, [props.state.songsOnAlbum]); diff --git a/client/src/components/windows/ArtistWindow.tsx b/client/src/components/windows/artist/ArtistWindow.tsx similarity index 71% rename from client/src/components/windows/ArtistWindow.tsx rename to client/src/components/windows/artist/ArtistWindow.tsx index 7b8ed4c..fd76abb 100644 --- a/client/src/components/windows/ArtistWindow.tsx +++ b/client/src/components/windows/artist/ArtistWindow.tsx @@ -1,13 +1,15 @@ import React, { useEffect, useState } from 'react'; import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core'; import PersonIcon from '@material-ui/icons/Person'; -import * as serverApi from '../../api'; -import { WindowState } from './Windows'; -import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; -import EditableText from '../common/EditableText'; -import SubmitChangesButton from '../common/SubmitChangesButton'; -import SongTable, { SongGetters } from '../tables/ResultsTable'; -import { saveArtistChanges } from '../../lib/saveChanges'; +import * as serverApi from '../../../api'; +import { WindowState } from '../Windows'; +import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; +import EditableText from '../../common/EditableText'; +import SubmitChangesButton from '../../common/SubmitChangesButton'; +import SongTable, { SongGetters } from '../../tables/ResultsTable'; +import { saveArtistChanges } from '../../../lib/saveChanges'; +import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; +import { queryArtists, querySongs } from '../../../lib/backend/queries'; var _ = require('lodash'); export type ArtistMetadata = serverApi.ArtistDetails; @@ -50,38 +52,15 @@ export interface IProps { } export async function getArtistMetadata(id: number) { - const query = { - prop: serverApi.QueryElemProperty.artistId, - propOperand: id, - propOperator: serverApi.QueryFilterOp.Eq, - }; - - var q: serverApi.QueryRequest = { - query: query, - offsetsLimits: { - artistOffset: 0, - artistLimit: 1, - }, - ordering: { - orderBy: { - type: serverApi.OrderByType.Name, - }, - ascending: true, + return (await queryArtists({ + query: { + a: QueryLeafBy.ArtistId, + b: id, + leafOp: QueryLeafOp.Equals, }, - }; - - const requestOpts = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(q), - }; - - return (async () => { - const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) - let json: any = await response.json(); - let artist = json.artists[0]; - return artist; - })(); + offset: 0, + limit: 1, + }))[0]; } export default function ArtistWindow(props: IProps) { @@ -103,37 +82,20 @@ export default function ArtistWindow(props: IProps) { useEffect(() => { if (props.state.songsByArtist) { return; } - var q: serverApi.QueryRequest = { - query: { - prop: serverApi.QueryElemProperty.artistId, - propOperator: serverApi.QueryFilterOp.Eq, - propOperand: props.state.artistId, - }, - offsetsLimits: { - songOffset: 0, - songLimit: 100, - }, - ordering: { - orderBy: { - type: serverApi.OrderByType.Name, - }, - ascending: true, - }, - }; - - const requestOpts = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(q), - }; - (async () => { - const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) - let json: any = await response.json(); + const songs = await querySongs({ + query: { + a: QueryLeafBy.ArtistId, + b: props.state.artistId, + leafOp: QueryLeafOp.Equals, + }, + offset: 0, + limit: -1, + }); props.dispatch({ type: ArtistWindowStateActions.SetSongs, - value: json.songs, - }); + value: songs, + }); })(); }, [props.state.songsByArtist]); diff --git a/client/src/components/windows/manage_tags/ManageTagMenu.tsx b/client/src/components/windows/manage_tags/ManageTagMenu.tsx new file mode 100644 index 0000000..5b5f9a1 --- /dev/null +++ b/client/src/components/windows/manage_tags/ManageTagMenu.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; +import { Menu, MenuItem, TextField, Input } from '@material-ui/core'; +import NestedMenuItem from "material-ui-nested-menu-item"; +import MenuEditText from '../../common/MenuEditText'; + +export function PickTag(props: { + tags: any[] + open: boolean + root: boolean + onPick: (v: string | null) => void +}) { + + return <> + {props.root && props.onPick(null)}>/} + {props.tags.map((tag: any) => { + if ('children' in tag && tag.children.length > 0) { + return props.onPick(tag.tagId.toString())} + > + + + } + return props.onPick(tag.tagId.toString())}>{tag.name} + }) + } +} + +export default function ManageTagMenu(props: { + position: null | number[], + open: boolean, + onClose: () => void, + onRename: (s: string) => void, + onDelete: () => void, + onMove: (to: string | null) => void, + onMergeInto: (to: string) => void, + onOpenInTab: () => void, + tag: any, + changedTags: any[], // Tags organized hierarchically with "children" fields +}) { + const pos = props.open && props.position ? + { left: props.position[0], top: props.position[1] } + : { left: 0, top: 0 } + + return + { + props.onClose(); + props.onOpenInTab(); + }} + >Browse + { + props.onClose(); + props.onDelete(); + }} + >Delete + + { + props.onClose(); + props.onRename(s); + }} + /> + + + { + props.onClose(); + props.onMove(v); + }} /> + + + { + if(v === null) { return; } + props.onClose(); + props.onMergeInto(v); + }} /> + + +} \ No newline at end of file diff --git a/client/src/components/windows/manage_tags/ManageTagsWindow.tsx b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx new file mode 100644 index 0000000..d9e48ea --- /dev/null +++ b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx @@ -0,0 +1,479 @@ +import React, { useEffect, useState, ReactFragment } from 'react'; +import { WindowState, newWindowReducer, WindowType } from '../Windows'; +import { Box, Typography, Chip, IconButton, useTheme, Button } from '@material-ui/core'; +import LoyaltyIcon from '@material-ui/icons/Loyalty'; +import ArrowRightIcon from '@material-ui/icons/ArrowRight'; +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; +import ManageTagMenu from './ManageTagMenu'; +import ControlTagChanges, { TagChange, TagChangeType, submitTagChanges } from './TagChange'; +import { queryTags } from '../../../lib/backend/queries'; +import NewTagMenu from './NewTagMenu'; +import { v4 as genUuid } from 'uuid'; +import { MainWindowStateActions } from '../../MainWindow'; +import LocalOfferIcon from '@material-ui/icons/LocalOffer'; +import { songGetters } from '../../../lib/songGetters'; +import Alert from '@material-ui/lab/Alert'; +var _ = require('lodash'); + +export interface ManageTagsWindowState extends WindowState { + // Tags are indexed by a string ID. This can be a stringified MuDBase ID integer, + // or a UID for tags which only exist in the front-end and haven't been committed + // to the database. + fetchedTags: Record | null, + pendingChanges: TagChange[], + alert: ReactFragment | null, // For notifications such as errors +} + +export enum ManageTagsWindowActions { + SetFetchedTags = "SetFetchedTags", + SetPendingChanges = "SetPendingChanges", + Reset = "Reset", + SetAlert = "SetAlert", +} + +export function ManageTagsWindowReducer(state: ManageTagsWindowState, action: any) { + switch (action.type) { + case ManageTagsWindowActions.SetFetchedTags: + return { + ...state, + fetchedTags: action.value, + } + case ManageTagsWindowActions.SetPendingChanges: + return { + ...state, + pendingChanges: action.value, + } + case ManageTagsWindowActions.Reset: + return { + ...state, + pendingChanges: [], + fetchedTags: null, + alert: null, + } + case ManageTagsWindowActions.SetAlert: + return { + ...state, + alert: action.value, + } + default: + throw new Error("Unimplemented ManageTagsWindow state update.") + } +} + +export function organiseTags(allTags: Record, fromId: string | null): any[] { + const base = Object.values(allTags).filter((tag: any) => { + var par: any = ("proposedParent" in tag) ? tag.proposedParent : tag.parentId; + + return (fromId === null && !par) || + (par && par === fromId) + }); + + return base.map((tag: any) => { + return { + ...tag, + children: organiseTags(allTags, tag.tagId), + } + }); +} + +export async function getAllTags() { + return (async () => { + var retval: Record = {}; + const tags = await queryTags({ + query: undefined, + offset: 0, + limit: -1, + }); + // Convert numeric IDs to string IDs because that is + // what we work with within this component. + tags.forEach((tag: any) => { + retval[tag.tagId.toString()] = { + ...tag, + tagId: tag.tagId && tag.tagId.toString(), + parentId: tag.parentId && tag.parentId.toString(), + childIds: tag.childIds && tag.childIds.map((c: number) => c.toString()), + } + }); + return retval; + })(); +} + +export function ExpandArrow(props: { + expanded: boolean, + onSetExpanded: (v: boolean) => void, +}) { + return props.expanded ? + props.onSetExpanded(false)}> : + props.onSetExpanded(true)}>; +} + +export function CreateTagButton(props: any) { + return + + { }} /> + + + +} + +export function SingleTag(props: { + tag: any, + prependElems: any[], + dispatch: (action: any) => void, + mainDispatch: (action: any) => void, + state: ManageTagsWindowState, + changedTags: any[], +}) { + const tag = props.tag; + const hasChildren = 'children' in tag && tag.children.length > 0; + + const [menuPos, setMenuPos] = React.useState(null); + const [expanded, setExpanded] = useState(false); + const theme = useTheme(); + + const onOpenMenu = (e: any) => { + setMenuPos([e.clientX, e.clientY]) + }; + const onCloseMenu = () => { + setMenuPos(null); + }; + + var tagLabel: any = tag.name; + if ("proposedName" in tag) { + tagLabel = <>{tag.name}→{tag.proposedName}; + } else if ("proposeDelete" in tag && tag.proposeDelete) { + tagLabel = <>{tag.name}; + } + + const TagChip = (props: any) => + + ; + + return <> + + + + + {props.prependElems} + + + {hasChildren && expanded && tag.children.map((child: any) => , + /]} + dispatch={props.dispatch} + mainDispatch={props.mainDispatch} + state={props.state} + changedTags={props.changedTags} + />)} + { + props.mainDispatch({ + type: MainWindowStateActions.AddTab, + tabState: { + tabLabel: <>{tag.name}, + tagId: tag.tagId, + metadata: null, + songGetters: songGetters, + songsWithTag: null, + }, + tabReducer: newWindowReducer[WindowType.Tag], + tabType: WindowType.Tag, + }) + }} + onRename={(s: string) => { + props.dispatch({ + type: ManageTagsWindowActions.SetPendingChanges, + value: [ + ...props.state.pendingChanges, + { + type: TagChangeType.Rename, + name: s, + id: tag.tagId, + } + ] + }) + props.dispatch({ + type: ManageTagsWindowActions.SetAlert, + value: null, + }) + }} + onDelete={() => { + props.dispatch({ + type: ManageTagsWindowActions.SetPendingChanges, + value: [ + ...props.state.pendingChanges, + { + type: TagChangeType.Delete, + id: tag.tagId, + } + ] + }) + props.dispatch({ + type: ManageTagsWindowActions.SetAlert, + value: null, + }) + }} + onMove={(to: string | null) => { + props.dispatch({ + type: ManageTagsWindowActions.SetPendingChanges, + value: [ + ...props.state.pendingChanges, + { + type: TagChangeType.MoveTo, + id: tag.tagId, + parent: to, + } + ] + }) + props.dispatch({ + type: ManageTagsWindowActions.SetAlert, + value: null, + }) + }} + onMergeInto={(into: string) => { + props.dispatch({ + type: ManageTagsWindowActions.SetPendingChanges, + value: [ + ...props.state.pendingChanges, + { + type: TagChangeType.MergeTo, + id: tag.tagId, + into: into, + } + ] + }) + props.dispatch({ + type: ManageTagsWindowActions.SetAlert, + value: null, + }) + }} + tag={tag} + changedTags={props.changedTags} + /> + +} + +function annotateTagsWithChanges(tags: Record, changes: TagChange[]) { + var retval: Record = _.cloneDeep(tags); + + const applyDelete = (id: string) => { + retval[id].proposeDelete = true; + Object.values(tags).filter((t: any) => t.parentId === id) + .forEach((child: any) => applyDelete(child.tagId)); + } + + changes.forEach((change: TagChange) => { + switch (change.type) { + case TagChangeType.Rename: + retval[change.id].proposedName = change.name; + break; + case TagChangeType.Delete: + applyDelete(change.id); + break; + case TagChangeType.MoveTo: + retval[change.id].proposedParent = change.parent; + break; + case TagChangeType.MergeTo: + retval[change.id].proposedMergeInto = change.into; + break; + case TagChangeType.Create: + retval[change.id] = { + isNewTag: true, + name: change.name, + parentId: change.parent, + tagId: change.id, + } + if (change.parent) { + retval[change.parent].childIds = + [...retval[change.parent].childIds, change.id] + } + break; + default: + throw new Error("Unimplemented tag change") + } + }) + return retval; +} + +function applyTagsChanges(tags: Record, changes: TagChange[]) { + var retval = _.cloneDeep(tags); + + const applyDelete = (id: string) => { + Object.values(tags).filter((t: any) => t.parentId === id) + .forEach((child: any) => applyDelete(child.tagId)); + delete retval[id].proposeDelete; + } + + changes.forEach((change: TagChange) => { + switch (change.type) { + case TagChangeType.Rename: + retval[change.id].name = change.name; + break; + case TagChangeType.Delete: + applyDelete(change.id); + break; + case TagChangeType.MoveTo: + retval[change.id].parentId = change.parent; + if (change.parent === null) { delete retval[change.id].parentId; } + break; + case TagChangeType.MergeTo: + applyDelete(change.id); + break; + case TagChangeType.Create: + retval[change.id] = { + name: change.name, + tagId: change.id, + parentId: change.parent, + isNewTag: true, + } + if (change.parent) { + retval[change.parent].childIds = + [...retval[change.parent].childIds, change.id] + } + break; + default: + throw new Error("Unimplemented tag change") + } + }) + return retval; +} + +export default function ManageTagsWindow(props: { + state: ManageTagsWindowState, + dispatch: (action: any) => void, + mainDispatch: (action: any) => void, +}) { + const [newTagMenuPos, setNewTagMenuPos] = React.useState(null); + + const onOpenNewTagMenu = (e: any) => { + setNewTagMenuPos([e.clientX, e.clientY]) + }; + const onCloseNewTagMenu = () => { + setNewTagMenuPos(null); + }; + + useEffect(() => { + if (props.state.fetchedTags !== null) { + return; + } + (async () => { + const allTags = await getAllTags(); + // We have the tags in list form. Now, we want to organize + // them hierarchically by giving each tag a "children" prop. + props.dispatch({ + type: ManageTagsWindowActions.SetFetchedTags, + value: allTags, + }); + })(); + }, [props.state.fetchedTags]); + + const tagsWithChanges = annotateTagsWithChanges(props.state.fetchedTags || {}, props.state.pendingChanges) + const changedTags = organiseTags( + applyTagsChanges(props.state.fetchedTags || {}, props.state.pendingChanges), + null); + const tags = organiseTags(tagsWithChanges, null); + + return <> + + + + + + Manage Tags + + {props.state.pendingChanges.length > 0 && + { + props.dispatch({ + type: ManageTagsWindowActions.SetPendingChanges, + value: [], + }) + props.dispatch({ + type: ManageTagsWindowActions.SetAlert, + value: null, + }) + }} + onSave={() => { + submitTagChanges(props.state.pendingChanges).then(() => { + props.dispatch({ + type: ManageTagsWindowActions.Reset + }); + }).catch((e: Error) => { + props.dispatch({ + type: ManageTagsWindowActions.SetAlert, + value: Failed to save changes: {e.message}, + }) + }) + }} + getTagDetails={(id: string) => tagsWithChanges[id]} + /> + } + {props.state.alert && {props.state.alert}} + + {tags && tags.length && tags.map((tag: any) => { + return ; + })} + { onOpenNewTagMenu(e) }} /> + + + { + props.dispatch({ + type: ManageTagsWindowActions.SetPendingChanges, + value: [ + ...props.state.pendingChanges, + { + type: TagChangeType.Create, + id: genUuid(), + parent: parentId, + name: name, + } + ] + }) + }} + onClose={onCloseNewTagMenu} + changedTags={changedTags} + /> + +} \ No newline at end of file diff --git a/client/src/components/windows/manage_tags/NewTagMenu.tsx b/client/src/components/windows/manage_tags/NewTagMenu.tsx new file mode 100644 index 0000000..3d16315 --- /dev/null +++ b/client/src/components/windows/manage_tags/NewTagMenu.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import { Menu, MenuItem, TextField, Input } from '@material-ui/core'; +import NestedMenuItem from "material-ui-nested-menu-item"; +import MenuEditText from '../../common/MenuEditText'; + +export function PickCreateTag(props: { + tags: any[], + open: boolean, + parentId: string | null, + onCreate: (name: string, parentId: string | null) => void, +}) { + + return <> + { + props.onCreate(s, props.parentId); + }} + /> + {props.tags.map((tag: any) => { + return + + + })} + +} + +export default function NewTagMenu(props: { + position: null | number[], + open: boolean, + onCreate: (name: string, parentId: string | null) => void, + onClose: () => void, + changedTags: any[], // Tags organized hierarchically with "children" fields +}) { + const pos = props.open && props.position ? + { left: props.position[0], top: props.position[1] } + : { left: 0, top: 0 } + + return + { + props.onClose(); + props.onCreate(n, v); + }} /> + +} \ No newline at end of file diff --git a/client/src/components/windows/manage_tags/TagChange.tsx b/client/src/components/windows/manage_tags/TagChange.tsx new file mode 100644 index 0000000..cce1c06 --- /dev/null +++ b/client/src/components/windows/manage_tags/TagChange.tsx @@ -0,0 +1,147 @@ +import React, { useState, useEffect } from 'react'; +import { Typography, Chip, CircularProgress, Box, Paper } from '@material-ui/core'; +import { queryTags } from '../../../lib/backend/queries'; +import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; +import DiscardChangesButton from '../../common/DiscardChangesButton'; +import SubmitChangesButton from '../../common/SubmitChangesButton'; +import { createTag, modifyTag, deleteTag, mergeTag } from '../../../lib/backend/tags'; + +export enum TagChangeType { + Delete = "Delete", + Create = "Create", + MoveTo = "MoveTo", + MergeTo = "MergeTo", + Rename = "Rename", +} + +export interface TagChange { + type: TagChangeType, + id: string, // Stringified integer == MuDBase ID. Other string == not yet committed to DB. + parent?: string | null, // Stringified integer == MuDBase ID. Other string == not yet committed to DB. + // null refers to the tags root. + name?: string, + into?: string, // Used for storing the tag ID to merge into, if applicable. As in the other ID fields. +} + +export async function submitTagChanges(changes: TagChange[]) { + // Upon entering this function, some tags have a real numeric MuDBase ID (stringified), + // while others have a UUID string which is a placeholder until the tag is created. + // While applying the changes, UUIDs will be replaced by real numeric IDs. + // Therefore we maintain a lookup table for mapping the old to the new. + var id_lookup: Record = {} + + const getId = (id_string: string) => { + return (Number(id_string) === NaN) ? + id_lookup[id_string] : Number(id_string); + } + + for (const change of changes) { + // If string is of form "1", convert to ID number directly. + // Otherwise, look it up in the table. + const parentId = change.parent ? getId(change.parent) : undefined; + const numericId = change.id ? getId(change.id) : undefined; + const intoId = change.into ? getId(change.into) : undefined; + switch (change.type) { + case TagChangeType.Create: + if (!change.name) { throw new Error("Cannot create tag without name"); } + const { id } = await createTag({ + name: change.name, + parentId: parentId, + }); + id_lookup[change.id] = id; + break; + case TagChangeType.MoveTo: + if (!numericId) { throw new Error("Cannot modify tag with no numeric ID"); } + await modifyTag( + numericId, + { + parentId: parentId, + }) + break; + case TagChangeType.Rename: + if (!numericId) { throw new Error("Cannot modify tag with no numeric ID"); } + await modifyTag( + numericId, + { + name: change.name, + }) + break; + case TagChangeType.Delete: + if (!numericId) { throw new Error("Cannot delete tag with no numeric ID"); } + await deleteTag(numericId) + break; + case TagChangeType.MergeTo: + if (!numericId) { throw new Error("Cannot merge tag with no numeric ID"); } + if (!intoId) { throw new Error("Cannot merge tag into tag with no numeric ID"); } + await mergeTag(numericId, intoId); + break; + default: + throw new Error("Unimplemented tag change"); + } + } +} + +export function TagChangeDisplay(props: { + change: TagChange, + getTagDetails: (id: string) => any, +}) { + const tag = props.getTagDetails(props.change.id); + const oldParent = tag.parentId ? props.getTagDetails(tag.parentId) : null; + const newParent = props.change.parent ? props.getTagDetails(props.change.parent) : null; + + const MakeTag = (props: { name: string }) => + const MainTag = tag ? + : + ; + + switch (props.change.type) { + case TagChangeType.Delete: + return Delete {MainTag} + case TagChangeType.Rename: + const NewTag = tag ? + : + ; + return Rename {MainTag} to {NewTag} + case TagChangeType.MoveTo: + const OldParent = oldParent !== undefined ? + : + ; + const NewParent = newParent !== undefined ? + : + ; + return Move {MainTag} from {OldParent} to {NewParent} + case TagChangeType.MergeTo: + const intoTag = props.getTagDetails(props.change.into || "unknown"); + const IntoTag = + return Merge {MainTag} into {IntoTag} + case TagChangeType.Create: + return props.change.parent ? + Create {MainTag} under : + Create {MainTag} + default: + throw new Error("Unhandled tag change type") + } +} + +export default function ControlTagChanges(props: { + changes: TagChange[], + onSave: () => void, + onDiscard: () => void, + getTagDetails: (id: string) => any, +}) { + return + Pending changes + + {props.changes.map((change: any) => + + + + + )} + + + Save Changes + Discard Changes + + +} \ No newline at end of file diff --git a/client/src/components/windows/QueryWindow.tsx b/client/src/components/windows/query/QueryWindow.tsx similarity index 59% rename from client/src/components/windows/QueryWindow.tsx rename to client/src/components/windows/query/QueryWindow.tsx index e35d233..b410a9d 100644 --- a/client/src/components/windows/QueryWindow.tsx +++ b/client/src/components/windows/query/QueryWindow.tsx @@ -1,13 +1,13 @@ import React, { useEffect } from 'react'; import { createMuiTheme, Box, LinearProgress } from '@material-ui/core'; -import { QueryElem, toApiQuery } from '../../lib/query/Query'; -import QueryBuilder from '../querybuilder/QueryBuilder'; -import * as serverApi from '../../api'; -import SongTable from '../tables/ResultsTable'; -import { songGetters } from '../../lib/songGetters'; -import { getArtists, getSongTitles, getAlbums, getTags } from '../../lib/query/Getters'; +import { QueryElem, toApiQuery, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; +import QueryBuilder from '../../querybuilder/QueryBuilder'; +import * as serverApi from '../../../api'; +import SongTable from '../../tables/ResultsTable'; +import { songGetters } from '../../../lib/songGetters'; +import { queryArtists, querySongs, queryAlbums, queryTags } from '../../../lib/backend/queries'; import { grey } from '@material-ui/core/colors'; -import { WindowState } from './Windows'; +import { WindowState } from '../Windows'; var _ = require('lodash'); const darkTheme = createMuiTheme({ @@ -36,6 +36,56 @@ export enum QueryWindowStateActions { SetResultsForQuery = "setResultsForQuery", } +async function getArtistNames(filter: string) { + const artists = await queryArtists({ + query: filter.length > 0 ? { + a: QueryLeafBy.ArtistName, + b: '%' + filter + '%', + leafOp: QueryLeafOp.Like + } : undefined, + offset: 0, + limit: -1, + }); + + return [...(new Set([...(artists.map((a:any) => a.name))]))]; +} + +async function getAlbumNames(filter: string) { + const albums = await queryAlbums({ + query: filter.length > 0 ? { + a: QueryLeafBy.AlbumName, + b: '%' + filter + '%', + leafOp: QueryLeafOp.Like + } : undefined, + offset: 0, + limit: -1, + }); + + return [...(new Set([...(albums.map((a:any) => a.name))]))]; +} + +async function getSongTitles(filter: string) { + const songs = await querySongs({ + query: filter.length > 0 ? { + a: QueryLeafBy.SongTitle, + b: '%' + filter + '%', + leafOp: QueryLeafOp.Like + } : undefined, + offset: 0, + limit: -1, + }); + + return [...(new Set([...(songs.map((s:any) => s.title))]))]; +} + +async function getTagItems() { + return await queryTags({ + query: undefined, + offset: 0, + limit: -1, + }); +} + export function QueryWindowReducer(state: QueryWindowState, action: any) { switch (action.type) { case QueryWindowStateActions.SetQuery: @@ -73,36 +123,18 @@ export default function QueryWindow(props: IProps) { const showResults = (query && resultsFor && query == resultsFor.for) ? resultsFor.results : []; const doQuery = async (_query: QueryElem) => { - var q: serverApi.QueryRequest = { - query: toApiQuery(_query), - offsetsLimits: { - songOffset: 0, - songLimit: 100, - }, - ordering: { - orderBy: { - type: serverApi.OrderByType.Name, - }, - ascending: true, - }, - }; - - const requestOpts = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(q), - }; - - return (async () => { - const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) - let json: any = await response.json(); - if (_.isEqual(query, _query)) { - setResultsForQuery({ - for: _query, - results: json.songs, - }) - } - })(); + const songs = await querySongs({ + query: _query, + offset: 0, + limit: 100, //TODO: pagination + }); + + if (_.isEqual(query, _query)) { + setResultsForQuery({ + for: _query, + results: songs, + }) + } } useEffect(() => { @@ -124,10 +156,10 @@ export default function QueryWindow(props: IProps) { editing={editing} onChangeEditing={setEditingQuery} requestFunctions={{ - getArtists: getArtists, + getArtists: getArtistNames, getSongTitles: getSongTitles, - getAlbums: getAlbums, - getTags: getTags, + getAlbums: getAlbumNames, + getTags: getTagItems, }} /> diff --git a/client/src/components/windows/SongWindow.tsx b/client/src/components/windows/song/SongWindow.tsx similarity index 80% rename from client/src/components/windows/SongWindow.tsx rename to client/src/components/windows/song/SongWindow.tsx index bf69c84..e7048aa 100644 --- a/client/src/components/windows/SongWindow.tsx +++ b/client/src/components/windows/song/SongWindow.tsx @@ -3,14 +3,16 @@ import { Box, Typography, IconButton, Button, CircularProgress } from '@material import AudiotrackIcon from '@material-ui/icons/Audiotrack'; import PersonIcon from '@material-ui/icons/Person'; import AlbumIcon from '@material-ui/icons/Album'; -import * as serverApi from '../../api'; -import { WindowState } from './Windows'; -import { ArtistMetadata } from './ArtistWindow'; -import { AlbumMetadata } from './AlbumWindow'; -import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; -import EditableText from '../common/EditableText'; -import SubmitChangesButton from '../common/SubmitChangesButton'; -import { saveSongChanges } from '../../lib/saveChanges'; +import * as serverApi from '../../../api'; +import { WindowState } from '../Windows'; +import { ArtistMetadata } from '../artist/ArtistWindow'; +import { AlbumMetadata } from '../album/AlbumWindow'; +import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; +import EditableText from '../../common/EditableText'; +import SubmitChangesButton from '../../common/SubmitChangesButton'; +import { saveSongChanges } from '../../../lib/saveChanges'; +import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; +import { querySongs } from '../../../lib/backend/queries'; export type SongMetadata = serverApi.SongDetails; export type SongMetadataChanges = serverApi.ModifySongRequest; @@ -47,38 +49,15 @@ export interface IProps { } export async function getSongMetadata(id: number) { - const query = { - prop: serverApi.QueryElemProperty.songId, - propOperand: id, - propOperator: serverApi.QueryFilterOp.Eq, - }; - - var q: serverApi.QueryRequest = { - query: query, - offsetsLimits: { - songOffset: 0, - songLimit: 1, - }, - ordering: { - orderBy: { - type: serverApi.OrderByType.Name, - }, - ascending: true, + return (await querySongs({ + query: { + a: QueryLeafBy.SongId, + b: id, + leafOp: QueryLeafOp.Equals, }, - }; - - const requestOpts = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(q), - }; - - return (async () => { - const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) - let json: any = await response.json(); - let song = json.songs[0]; - return song; - })(); + offset: 0, + limit: 1, + }))[0]; } export default function SongWindow(props: IProps) { diff --git a/client/src/components/windows/TagWindow.tsx b/client/src/components/windows/tag/TagWindow.tsx similarity index 70% rename from client/src/components/windows/TagWindow.tsx rename to client/src/components/windows/tag/TagWindow.tsx index 676aef5..ec33f91 100644 --- a/client/src/components/windows/TagWindow.tsx +++ b/client/src/components/windows/tag/TagWindow.tsx @@ -1,13 +1,15 @@ import React, { useEffect, useState } from 'react'; import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core'; import LocalOfferIcon from '@material-ui/icons/LocalOffer'; -import * as serverApi from '../../api'; -import { WindowState } from './Windows'; -import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; -import EditableText from '../common/EditableText'; -import SubmitChangesButton from '../common/SubmitChangesButton'; -import SongTable, { SongGetters } from '../tables/ResultsTable'; -import { saveTagChanges } from '../../lib/saveChanges'; +import * as serverApi from '../../../api'; +import { WindowState } from '../Windows'; +import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; +import EditableText from '../../common/EditableText'; +import SubmitChangesButton from '../../common/SubmitChangesButton'; +import SongTable, { SongGetters } from '../../tables/ResultsTable'; +import { saveTagChanges } from '../../../lib/saveChanges'; +import { queryTags, querySongs } from '../../../lib/backend/queries'; +import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; var _ = require('lodash'); export interface FullTagMetadata extends serverApi.TagDetails { @@ -55,49 +57,27 @@ export interface IProps { } export async function getTagMetadata(id: number) { - const query = { - prop: serverApi.QueryElemProperty.tagId, - propOperand: id, - propOperator: serverApi.QueryFilterOp.Eq, - }; - - var q: serverApi.QueryRequest = { - query: query, - offsetsLimits: { - tagOffset: 0, - tagLimit: 1, - }, - ordering: { - orderBy: { - type: serverApi.OrderByType.Name, - }, - ascending: true, + var tag = (await queryTags({ + query: { + a: QueryLeafBy.TagId, + b: id, + leafOp: QueryLeafOp.Equals, }, - }; - - const requestOpts = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(q), - }; - - return (async () => { - const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) - let json: any = await response.json(); - let tag = json.tags[0]; - - // Recursively fetch parent tags to build the full metadata. - if (tag.parentId) { - const parent = await getTagMetadata(tag.parentId); - tag.fullName = [...parent.fullName, tag.name]; - tag.fullId = [...parent.fullId, tag.tagId]; - } else { - tag.fullName = [tag.name]; - tag.fullId = [tag.tagId]; - } + offset: 0, + limit: 1, + }))[0]; + + // Recursively fetch parent tags to build the full metadata. + if (tag.parentId) { + const parent = await getTagMetadata(tag.parentId); + tag.fullName = [...parent.fullName, tag.name]; + tag.fullId = [...parent.fullId, tag.tagId]; + } else { + tag.fullName = [tag.name]; + tag.fullId = [tag.tagId]; + } - return tag; - })(); + return tag; } export default function TagWindow(props: IProps) { @@ -119,37 +99,20 @@ export default function TagWindow(props: IProps) { useEffect(() => { if (props.state.songsWithTag) { return; } - var q: serverApi.QueryRequest = { - query: { - prop: serverApi.QueryElemProperty.tagId, - propOperator: serverApi.QueryFilterOp.Eq, - propOperand: props.state.tagId, - }, - offsetsLimits: { - songOffset: 0, - songLimit: 100, - }, - ordering: { - orderBy: { - type: serverApi.OrderByType.Name, - }, - ascending: true, - }, - }; - - const requestOpts = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(q), - }; - (async () => { - const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) - let json: any = await response.json(); + const songs = await querySongs({ + query: { + a: QueryLeafBy.TagId, + b: props.state.tagId, + leafOp: QueryLeafOp.Equals, + }, + offset: 0, + limit: -1, + }); props.dispatch({ type: TagWindowStateActions.SetSongs, - value: json.songs, - }); + value: songs, + }); })(); }, [props.state.songsWithTag]); diff --git a/client/src/lib/query/Getters.tsx b/client/src/lib/backend/queries.tsx similarity index 67% rename from client/src/lib/query/Getters.tsx rename to client/src/lib/backend/queries.tsx index 8d16129..d93ef40 100644 --- a/client/src/lib/query/Getters.tsx +++ b/client/src/lib/backend/queries.tsx @@ -1,17 +1,18 @@ import * as serverApi from '../../api'; +import { QueryElem, toApiQuery } from '../query/Query'; -export async function getArtists(filter: string) { - const query = filter.length > 0 ? { - prop: serverApi.QueryElemProperty.artistName, - propOperand: filter, - propOperator: serverApi.QueryFilterOp.Like, - } : {}; +export interface QueryArgs { + query?: QueryElem, + offset: number, + limit: number, +} +export async function queryArtists(args: QueryArgs) { var q: serverApi.QueryRequest = { - query: query, + query: args.query ? toApiQuery(args.query) : {}, offsetsLimits: { - artistOffset: 0, - artistLimit: 100, + artistOffset: args.offset, + artistLimit: args.limit, }, ordering: { orderBy: { @@ -30,23 +31,16 @@ export async function getArtists(filter: string) { return (async () => { const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) let json: any = await response.json(); - const names: string[] = json.artists.map((elem: any) => { return elem.name; }); - return [...new Set(names)]; + return json.artists; })(); } -export async function getAlbums(filter: string) { - const query = filter.length > 0 ? { - prop: serverApi.QueryElemProperty.albumName, - propOperand: filter, - propOperator: serverApi.QueryFilterOp.Like, - } : {}; - +export async function queryAlbums(args: QueryArgs) { var q: serverApi.QueryRequest = { - query: query, + query: args.query ? toApiQuery(args.query) : {}, offsetsLimits: { - albumOffset: 0, - albumLimit: 100, + albumOffset: args.offset, + albumLimit: args.limit, }, ordering: { orderBy: { @@ -65,23 +59,16 @@ export async function getAlbums(filter: string) { return (async () => { const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) let json: any = await response.json(); - const names: string[] = json.albums.map((elem: any) => { return elem.name; }); - return [...new Set(names)]; + return json.albums; })(); } -export async function getSongTitles(filter: string) { - const query = filter.length > 0 ? { - prop: serverApi.QueryElemProperty.songTitle, - propOperand: filter, - propOperator: serverApi.QueryFilterOp.Like, - } : {}; - +export async function querySongs(args: QueryArgs) { var q: serverApi.QueryRequest = { - query: query, + query: args.query ? toApiQuery(args.query) : {}, offsetsLimits: { - songOffset: 0, - songLimit: 100, + songOffset: args.offset, + songLimit: args.limit, }, ordering: { orderBy: { @@ -100,17 +87,16 @@ export async function getSongTitles(filter: string) { return (async () => { const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) let json: any = await response.json(); - const titles: string[] = json.songs.map((elem: any) => { return elem.title; }); - return [...new Set(titles)]; + return json.songs; })(); } -export async function getTags() { +export async function queryTags(args: QueryArgs) { var q: serverApi.QueryRequest = { - query: {}, + query: args.query ? toApiQuery(args.query) : {}, offsetsLimits: { - tagOffset: 0, - tagLimit: 100, + tagOffset: args.offset, + tagLimit: args.limit, }, ordering: { orderBy: { diff --git a/client/src/lib/backend/tags.tsx b/client/src/lib/backend/tags.tsx new file mode 100644 index 0000000..67146e9 --- /dev/null +++ b/client/src/lib/backend/tags.tsx @@ -0,0 +1,61 @@ +import * as serverApi from '../../api'; + +export async function createTag(details: serverApi.CreateTagRequest) { + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(details), + }; + + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.CreateTagEndpoint, requestOpts) + if (!response.ok) { + throw new Error("Response to tag creation not OK: " + JSON.stringify(response)); + } + return await response.json(); +} + +export async function modifyTag(id: number, details: serverApi.ModifyTagRequest) { + const requestOpts = { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(details), + }; + + const response = await fetch( + (process.env.REACT_APP_BACKEND || "") + serverApi.ModifyTagEndpoint.replace(':id', id.toString()), + requestOpts + ); + if (!response.ok) { + throw new Error("Response to tag modification not OK: " + JSON.stringify(response)); + } +} + +export async function deleteTag(id: number) { + const requestOpts = { + method: 'DELETE', + }; + + const response = await fetch( + (process.env.REACT_APP_BACKEND || "") + serverApi.DeleteTagEndpoint.replace(':id', id.toString()), + requestOpts + ); + if (!response.ok) { + throw new Error("Response to tag deletion not OK: " + JSON.stringify(response)); + } +} + +export async function mergeTag(fromId: number, toId: number) { + const requestOpts = { + method: 'POST', + }; + + const response = await fetch( + (process.env.REACT_APP_BACKEND || "") + serverApi.MergeTagEndpoint + .replace(':id', fromId.toString()) + .replace(':toId', toId.toString()), + requestOpts + ); + if (!response.ok) { + throw new Error("Response to tag merge not OK: " + JSON.stringify(response)); + } +} \ No newline at end of file diff --git a/client/src/lib/query/Query.tsx b/client/src/lib/query/Query.tsx index 9109cfd..225d118 100644 --- a/client/src/lib/query/Query.tsx +++ b/client/src/lib/query/Query.tsx @@ -2,9 +2,13 @@ import * as serverApi from '../../api'; export enum QueryLeafBy { ArtistName = 0, + ArtistId, AlbumName, + AlbumId, TagInfo, - SongTitle + TagId, + SongTitle, + SongId, } export enum QueryLeafOp { @@ -14,11 +18,11 @@ export enum QueryLeafOp { } export interface TagQueryInfo { - fullName: string[], matchIds: number[], + fullName: string[], } export function isTagQueryInfo(e: any): e is TagQueryInfo { - return (typeof e === 'object') && 'fullName' in e && 'matchIds' in e; + return (typeof e === 'object') && 'matchIds' in e && 'fullName' in e; } export type QueryLeafOperand = string | number | TagQueryInfo; @@ -166,6 +170,10 @@ export function toApiQuery(q: QueryElem) : serverApi.Query { [QueryLeafBy.SongTitle]: serverApi.QueryElemProperty.songTitle, [QueryLeafBy.ArtistName]: serverApi.QueryElemProperty.artistName, [QueryLeafBy.AlbumName]: serverApi.QueryElemProperty.albumName, + [QueryLeafBy.AlbumId]: serverApi.QueryElemProperty.albumId, + [QueryLeafBy.ArtistId]: serverApi.QueryElemProperty.artistId, + [QueryLeafBy.TagId]: serverApi.QueryElemProperty.tagId, + [QueryLeafBy.SongId]: serverApi.QueryElemProperty.songId, } const leafOpsMapping: any = { [QueryLeafOp.Equals]: serverApi.QueryFilterOp.Eq, diff --git a/server/app.ts b/server/app.ts index 8d1922a..3155d73 100644 --- a/server/app.ts +++ b/server/app.ts @@ -15,6 +15,8 @@ import { TagDetailsEndpointHandler } from './endpoints/TagDetailsEndpointHandler import { CreateAlbumEndpointHandler } from './endpoints/CreateAlbumEndpointHandler'; import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbumEndpointHandler'; import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetailsEndpointHandler'; +import { DeleteTagEndpointHandler } from './endpoints/DeleteTagEndpointHandler'; +import { MergeTagEndpointHandler } from './endpoints/MergeTagEndpointHandler'; import * as endpointTypes from './endpoints/types'; const invokeHandler = (handler:endpointTypes.EndpointHandler, knex: Knex) => { @@ -53,6 +55,8 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => { app.post(apiBaseUrl + api.CreateAlbumEndpoint, invokeWithKnex(CreateAlbumEndpointHandler)); app.put(apiBaseUrl + api.ModifyAlbumEndpoint, invokeWithKnex(ModifyAlbumEndpointHandler)); app.get(apiBaseUrl + api.AlbumDetailsEndpoint, invokeWithKnex(AlbumDetailsEndpointHandler)); + app.delete(apiBaseUrl + api.DeleteTagEndpoint, invokeWithKnex(DeleteTagEndpointHandler)); + app.post(apiBaseUrl + api.MergeTagEndpoint, invokeWithKnex(MergeTagEndpointHandler)); } export { SetupApp } \ No newline at end of file diff --git a/server/endpoints/DeleteTagEndpointHandler.ts b/server/endpoints/DeleteTagEndpointHandler.ts new file mode 100644 index 0000000..6d0ab94 --- /dev/null +++ b/server/endpoints/DeleteTagEndpointHandler.ts @@ -0,0 +1,76 @@ +import * as api from '../../client/src/api'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; +import Knex from 'knex'; + +async function getChildrenRecursive(id: number, trx: any) { + const directChildren = (await trx.select('id') + .from('tags') + .where({ 'parentId': id })).map((r: any) => r.id); + + const indirectChildrenPromises = directChildren.map( + (child: number) => getChildrenRecursive(child, trx) + ); + const indirectChildrenNested = await Promise.all(indirectChildrenPromises); + const indirectChildren = indirectChildrenNested.flat(); + + return [ + ...directChildren, + ...indirectChildren, + ] +} + +export const DeleteTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { + if (!api.checkDeleteTagRequest(req)) { + const e: EndpointError = { + internalMessage: 'Invalid DeleteTag request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + const reqObject: api.DeleteTagRequest = req.body; + + console.log("Delete Tag:", reqObject); + + await knex.transaction(async (trx) => { + try { + // Start retrieving any child tags. + + const childTagsPromise = + getChildrenRecursive(req.params.id, trx); + + // Start retrieving the tag itself. + const tagPromise = trx.select('id') + .from('tags') + .where({ id: req.params.id }) + .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) + + // Wait for the requests to finish. + var [tag, children] = await Promise.all([tagPromise, childTagsPromise]); + + // Merge all IDs. + const toDelete = [ tag, ...children ]; + console.log ("deleting tags: ", toDelete); + + // Check that we found all objects we need. + if (!tag) { + const e: EndpointError = { + internalMessage: 'Tag or parent does not exist for DeleteTag request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + + // Delete the tag and its children. + await trx('tags') + .whereIn('id', toDelete) + .del(); + + // Respond to the request. + res.status(200).send(); + + } catch (e) { + catchUnhandledErrors(e); + trx.rollback(); + } + }) +} \ No newline at end of file diff --git a/server/endpoints/MergeTagEndpointHandler.ts b/server/endpoints/MergeTagEndpointHandler.ts new file mode 100644 index 0000000..b84db4b --- /dev/null +++ b/server/endpoints/MergeTagEndpointHandler.ts @@ -0,0 +1,73 @@ +import * as api from '../../client/src/api'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; +import Knex from 'knex'; + +export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { + if (!api.checkMergeTagRequest(req)) { + const e: EndpointError = { + internalMessage: 'Invalid MergeTag request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + const reqObject: api.DeleteTagRequest = req.body; + + console.log("Merge Tag:", reqObject); + const fromId = req.params.id; + const toId = req.params.toId; + + await knex.transaction(async (trx) => { + try { + // Start retrieving the "from" tag. + const fromTagPromise = trx.select('id') + .from('tags') + .where({ id: fromId }) + .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) + + // Start retrieving the "to" tag. + const toTagPromise = trx.select('id') + .from('tags') + .where({ id: toId }) + .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) + + // Wait for the requests to finish. + var [fromTag, toTag] = await Promise.all([fromTagPromise, toTagPromise]); + + // Check that we found all objects we need. + if (!fromTag || !toTag) { + const e: EndpointError = { + internalMessage: 'Source or target tag does not exist for MergeTag request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + + // Assign new tag ID to any objects referencing the to-be-merged tag. + const cPromise = trx('tags') + .where({ 'parentId': fromId }) + .update({ 'parentId': toId }); + const sPromise = trx('songs_tags') + .where({ 'tagId': fromId }) + .update({ 'tagId': toId }); + const arPromise = trx('artists_tags') + .where({ 'tagId': fromId }) + .update({ 'tagId': toId }); + const alPromise = trx('albums_tags') + .where({ 'tagId': fromId }) + .update({ 'tagId': toId }); + await Promise.all([sPromise, arPromise, alPromise, cPromise]); + + // Delete the original tag. + await trx('tags') + .where({ 'id': fromId }) + .del(); + + // Respond to the request. + res.status(200).send(); + + } catch (e) { + catchUnhandledErrors(e); + trx.rollback(); + } + }) +} \ No newline at end of file diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index 00ad1d5..89fb35d 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -179,7 +179,7 @@ const objectColumns = { }; function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryElem, ordering: api.Ordering, - offset: number, limit: number) { + offset: number, limit: number | null) { const joinObjects = getRequiredDatabaseObjects(queryElem); joinObjects.delete(queryFor); // We are already querying this object in the base query. @@ -213,7 +213,12 @@ function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryEl (ordering.ascending ? 'asc' : 'desc')); // Apply limiting. - q = q.limit(limit).offset(offset); + if(limit !== null) { + q = q.limit(limit) + } + + // Apply offsetting. + q = q.offset(offset); return q; } @@ -271,43 +276,43 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, const albumLimit = reqObject.offsetsLimits.albumLimit; const albumOffset = reqObject.offsetsLimits.albumOffset; - const artistsPromise: Promise = (artistLimit && artistLimit > 0) ? + const artistsPromise: Promise = (artistLimit && artistLimit !== 0) ? constructQuery(knex, ObjectType.Artist, reqObject.query, reqObject.ordering, artistOffset || 0, - artistLimit + artistLimit >= 0 ? artistLimit : null, ) : (async () => [])(); - const albumsPromise: Promise = (albumLimit && albumLimit > 0) ? + const albumsPromise: Promise = (albumLimit && albumLimit !== 0) ? constructQuery(knex, ObjectType.Album, reqObject.query, reqObject.ordering, artistOffset || 0, - albumLimit + albumLimit >= 0 ? albumLimit : null, ) : (async () => [])(); - const songsPromise: Promise = (songLimit && songLimit > 0) ? + const songsPromise: Promise = (songLimit && songLimit !== 0) ? constructQuery(knex, ObjectType.Song, reqObject.query, reqObject.ordering, songOffset || 0, - songLimit + songLimit >= 0 ? songLimit : null, ) : (async () => [])(); - const tagsPromise: Promise = (tagLimit && tagLimit > 0) ? + const tagsPromise: Promise = (tagLimit && tagLimit !== 0) ? constructQuery(knex, ObjectType.Tag, reqObject.query, reqObject.ordering, tagOffset || 0, - tagLimit + tagLimit >= 0 ? tagLimit : null, ) : (async () => [])(); @@ -318,12 +323,12 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, const ids = songs.map((song: any) => song['songs.id']); return ids; })(); - const songsArtistsPromise: Promise> = (songLimit && songLimit > 0) ? + const songsArtistsPromise: Promise> = (songLimit && songLimit !== 0) ? (async () => { return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Artist, await songIdsPromise); })() : (async () => { return {}; })(); - const songsTagsPromise: Promise> = (songLimit && songLimit > 0) ? + const songsTagsPromise: Promise> = (songLimit && songLimit !== 0) ? (async () => { const tagsPerSong: Record = await getLinkedObjects(knex, ObjectType.Song, ObjectType.Tag, await songIdsPromise); var result: Record = {}; @@ -338,7 +343,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, return result; })() : (async () => { return {}; })(); - const songsAlbumsPromise: Promise> = (songLimit && songLimit > 0) ? + const songsAlbumsPromise: Promise> = (songLimit && songLimit !== 0) ? (async () => { return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Album, await songIdsPromise); })() :