From df57cd6bb563e3428e8aa49f0cad6f0a6a16e388 Mon Sep 17 00:00:00 2001 From: Yash Jipkate <34203227+YashJipkate@users.noreply.github.com> Date: Sat, 24 Apr 2021 04:07:08 +0530 Subject: [PATCH] Allow adding songs to multiple playlists at once. (#995) * Add support for multiple playlists * Fix lint * Remove console log comment * Disable 'check' when loading * Fix lint * reset playlists on closeAddToPlaylist * new playlist: accomodate string type on enter * Fix lint * multiple new playlists are added correctly * use makestyle() * Add tests * Fix lint --- ui/package-lock.json | 404 +++++++++++++++++++++ ui/package.json | 6 +- ui/src/dialogs/AddToPlaylistDialog.js | 80 ++-- ui/src/dialogs/AddToPlaylistDialog.test.js | 222 +++++++++++ ui/src/dialogs/SelectPlaylistInput.js | 48 ++- ui/src/dialogs/SelectPlaylistInput.test.js | 115 ++++++ ui/src/setupTests.js | 15 + 7 files changed, 834 insertions(+), 56 deletions(-) create mode 100644 ui/src/dialogs/AddToPlaylistDialog.test.js create mode 100644 ui/src/dialogs/SelectPlaylistInput.test.js diff --git a/ui/package-lock.json b/ui/package-lock.json index bda4bf244..6fa5b143d 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1905,6 +1905,15 @@ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz", "integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==" }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -4981,6 +4990,12 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "decimal.js": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", + "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==", + "dev": true + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -8028,6 +8043,12 @@ "isobject": "^3.0.1" } }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "is-regex": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", @@ -8664,6 +8685,363 @@ } } }, + "jest-environment-jsdom-sixteen": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom-sixteen/-/jest-environment-jsdom-sixteen-1.0.3.tgz", + "integrity": "sha512-CwMqDUUfSl808uGPWXlNA1UFkWFgRmhHvyAjhCmCry6mYq4b/nn80MMN7tglqo5XgrANIs/w+mzINPzbZ4ZZrQ==", + "dev": true, + "requires": { + "@jest/fake-timers": "^25.1.0", + "jest-mock": "^25.1.0", + "jest-util": "^25.1.0", + "jsdom": "^16.2.1" + }, + "dependencies": { + "@jest/fake-timers": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", + "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "lolex": "^5.0.0" + } + }, + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "acorn": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1.1.tgz", + "integrity": "sha512-xYiIVjNuqtKXMxlRMDc6mZUhXehod4a3gbZ1qRlM7icK4EbxUFNLhWoPblCvFtB2Y9CIqHP3CF/rdxLItaQv8g==", + "dev": true + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + } + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dev": true, + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true + } + } + }, + "escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-message-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", + "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^1.0.1" + } + }, + "jest-mock": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", + "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0" + } + }, + "jest-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", + "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "make-dir": "^3.0.0" + } + }, + "jsdom": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.5.3.tgz", + "integrity": "sha512-Qj1H+PEvUsOtdPJ056ewXM4UJPCi4hhLA8wpiz9F2YvsRBhuFsXxtrIFAgGBDynQA9isAMGE91PfUYbdMPXuTA==", + "dev": true, + "requires": { + "abab": "^2.0.5", + "acorn": "^8.1.0", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "html-encoding-sniffer": "^2.0.1", + "is-potential-custom-element-name": "^1.0.0", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "request": "^2.88.2", + "request-promise-native": "^1.0.9", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.4", + "xml-name-validator": "^3.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "picomatch": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "dev": true + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + } + }, + "tr46": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", + "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-url": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.5.0.tgz", + "integrity": "sha512-fy+R77xWv0AiqfLl4nuGUlQ3/6b5uNfQ4WAbGQVMYshCTCCPK9psC1nWh3XHuxGVCtlcDDQPQW1csmmIQo+fwg==", + "dev": true, + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^6.1.0" + } + }, + "ws": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "dev": true + } + } + }, "jest-environment-node": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.9.0.tgz", @@ -10324,6 +10702,15 @@ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz", "integrity": "sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==" }, + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -12878,6 +13265,17 @@ "ra-core": "^3.12.0" } }, + "ra-test": { + "version": "3.14.3", + "resolved": "https://registry.npmjs.org/ra-test/-/ra-test-3.14.3.tgz", + "integrity": "sha512-sU6BLsGampXvU+3KihOy4/8isUYOuh4YLqYwxbdQe7/D99nRZy+g9MNrZm9hOZ6ESqWK/Ce5sSL1EFSon7+New==", + "dev": true, + "requires": { + "@testing-library/react": "^11.2.3", + "classnames": "~2.2.5", + "lodash": "~4.17.5" + } + }, "ra-ui-materialui": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/ra-ui-materialui/-/ra-ui-materialui-3.12.0.tgz", @@ -15973,6 +16371,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", diff --git a/ui/package.json b/ui/package.json index 27b937d6a..268009827 100644 --- a/ui/package.json +++ b/ui/package.json @@ -33,12 +33,14 @@ "@testing-library/react-hooks": "^5.1.1", "@testing-library/user-event": "^13.1.2", "css-mediaquery": "^0.1.2", - "prettier": "^2.2.1" + "jest-environment-jsdom-sixteen": "^1.0.3", + "prettier": "^2.2.1", + "ra-test": "^3.14.3" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "test": "react-scripts test", + "test": "react-scripts test --env=jest-environment-jsdom-sixteen", "lint": "eslint -c node_modules/eslint-config-react-app/index.js src/**/*.js", "eject": "react-scripts eject", "prettier": "prettier --write src/*.js src/**/*.js", diff --git a/ui/src/dialogs/AddToPlaylistDialog.js b/ui/src/dialogs/AddToPlaylistDialog.js index 6590443c8..0d4b1aa9f 100644 --- a/ui/src/dialogs/AddToPlaylistDialog.js +++ b/ui/src/dialogs/AddToPlaylistDialog.js @@ -1,11 +1,6 @@ import React, { useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { - useCreate, - useDataProvider, - useNotify, - useTranslate, -} from 'react-admin' +import { useDataProvider, useNotify, useTranslate } from 'react-admin' import { Button, Dialog, @@ -19,8 +14,6 @@ import { openDuplicateSongWarning, } from '../actions' import { SelectPlaylistInput } from './SelectPlaylistInput' -import { httpClient } from '../dataProvider' -import { REST_URL } from '../consts' import DuplicateSongDialog from './DuplicateSongDialog' export const AddToPlaylistDialog = () => { @@ -37,17 +30,16 @@ export const AddToPlaylistDialog = () => { const [value, setValue] = useState({}) const [check, setCheck] = useState(false) const dataProvider = useDataProvider() - const [createAndAddToPlaylist] = useCreate( - 'playlist', - { name: value.name }, - { - onSuccess: ({ data }) => { - setValue(data) - addToPlaylist(data.id) - }, - onFailure: (error) => notify(`Error: ${error.message}`, 'warning'), - } - ) + const createAndAddToPlaylist = (playlistObject) => { + dataProvider + .create('playlist', { + data: { name: playlistObject.name }, + }) + .then((res) => { + addToPlaylist(res.data.id) + }) + .catch((error) => notify(`Error: ${error.message}`, 'warning')) + } const addToPlaylist = (playlistId, distinctIds) => { const trackIds = Array.isArray(distinctIds) ? distinctIds : selectedIds @@ -66,10 +58,11 @@ export const AddToPlaylistDialog = () => { }) } - const checkDuplicateSong = (playlistId) => { - httpClient(`${REST_URL}/playlist/${playlistId}`) + const checkDuplicateSong = (playlistObject) => { + dataProvider + .getOne('playlist', { id: playlistObject.id }) .then((res) => { - const { tracks } = JSON.parse(res.body) + const tracks = res.data.tracks if (tracks) { const dupSng = tracks.filter((song) => selectedIds.some((id) => id === song.id) @@ -77,11 +70,9 @@ export const AddToPlaylistDialog = () => { if (dupSng.length) { const dupIds = dupSng.map((song) => song.id) - return dispatch(openDuplicateSongWarning(dupIds)) + dispatch(openDuplicateSongWarning(dupIds)) } - return setCheck(true) } - setCheck(true) }) .catch((error) => { @@ -91,47 +82,49 @@ export const AddToPlaylistDialog = () => { } const handleSubmit = (e) => { - if (value.id) { - addToPlaylist(value.id) - } else { - createAndAddToPlaylist() - } + value.forEach((playlistObject) => { + if (playlistObject.id) { + addToPlaylist(playlistObject.id, playlistObject.distinctIds) + } else { + createAndAddToPlaylist(playlistObject) + } + }) setCheck(false) + setValue({}) dispatch(closeAddToPlaylist()) e.stopPropagation() } const handleClickClose = (e) => { setCheck(false) + setValue({}) dispatch(closeAddToPlaylist()) e.stopPropagation() } const handleChange = (pls) => { - if (pls.id) { - checkDuplicateSong(pls.id) - } else { - setCheck(true) - } + if (!value.length || pls.length > value.length) { + let newlyAdded = pls.slice(-1).pop() + if (newlyAdded.id) { + setCheck(false) + checkDuplicateSong(newlyAdded) + } else setCheck(true) + } else if (pls.length === 0) setCheck(false) setValue(pls) } const handleDuplicateClose = () => { dispatch(closeDuplicateSongDialog()) - dispatch(closeAddToPlaylist()) } const handleDuplicateSubmit = () => { - addToPlaylist(value.id) dispatch(closeDuplicateSongDialog()) - dispatch(closeAddToPlaylist()) } const handleSkip = () => { const distinctSongs = selectedIds.filter( (id) => duplicateIds.indexOf(id) < 0 ) - addToPlaylist(value.id, distinctSongs) + value.slice(-1).pop().distinctIds = distinctSongs dispatch(closeDuplicateSongDialog()) - dispatch(closeAddToPlaylist()) } return ( @@ -154,7 +147,12 @@ export const AddToPlaylistDialog = () => { - diff --git a/ui/src/dialogs/AddToPlaylistDialog.test.js b/ui/src/dialogs/AddToPlaylistDialog.test.js new file mode 100644 index 000000000..72d5825aa --- /dev/null +++ b/ui/src/dialogs/AddToPlaylistDialog.test.js @@ -0,0 +1,222 @@ +import * as React from 'react' +import { TestContext } from 'ra-test' +import { DataProviderContext } from 'react-admin' +import { cleanup, fireEvent, render, waitFor } from '@testing-library/react' +import { AddToPlaylistDialog } from './AddToPlaylistDialog' + +describe('AddToPlaylistDialog', () => { + afterEach(cleanup) + + let mockData = [ + { id: 'sample-id1', name: 'sample playlist 1', owner: 'admin' }, + { id: 'sample-id2', name: 'sample playlist 2', owner: 'admin' }, + ] + let mockIndexedData = { + 'sample-id1': { + id: 'sample-id1', + name: 'sample playlist 1', + owner: 'admin', + }, + 'sample-id2': { + id: 'sample-id2', + name: 'sample playlist 2', + owner: 'admin', + }, + } + let selectedIds = ['song-1', 'song-2'] + + it('adds distinct songs to already existing playlists', async () => { + let mockDataProvider = { + getList: jest.fn(() => + Promise.resolve({ data: mockData, total: mockData.length }) + ), + getOne: jest.fn(() => + Promise.resolve({ data: { id: 'song-3' }, total: 1 }) + ), + create: jest.fn(() => + Promise.resolve({ data: { id: 'created-id', name: 'created-name' } }) + ), + } + const testutils = render( + + + + + + ) + + fireEvent.change(document.activeElement, { target: { value: 'sample' } }) + fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }) + fireEvent.keyDown(document.activeElement, { key: 'Enter' }) + fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }) + fireEvent.keyDown(document.activeElement, { key: 'Enter' }) + await waitFor(() => { + expect(testutils.getByTestId('playlist-add')).not.toBeDisabled() + }) + fireEvent.click(testutils.getByTestId('playlist-add')) + await waitFor(() => { + expect(mockDataProvider.create).toHaveBeenNthCalledWith( + 1, + 'playlistTrack', + { + data: { ids: selectedIds }, + filter: { playlist_id: 'sample-id1' }, + } + ) + }) + await waitFor(() => { + expect(mockDataProvider.create).toHaveBeenNthCalledWith( + 2, + 'playlistTrack', + { + data: { ids: selectedIds }, + filter: { playlist_id: 'sample-id2' }, + } + ) + }) + }) + + let mockDataProvider = { + getList: jest.fn(() => + Promise.resolve({ data: mockData, total: mockData.length }) + ), + getOne: jest.fn(() => + Promise.resolve({ data: { id: 'song-3' }, total: 1 }) + ), + create: jest.fn(() => + Promise.resolve({ data: { id: 'created-id1', name: 'created-name' } }) + ), + } + + it('adds distinct songs to a new playlist', async () => { + const testutils = render( + + + + + + ) + + fireEvent.change(document.activeElement, { target: { value: 'sample' } }) + fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }) + fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }) + fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }) + fireEvent.keyDown(document.activeElement, { key: 'Enter' }) + await waitFor(() => { + expect(testutils.getByTestId('playlist-add')).not.toBeDisabled() + }) + fireEvent.click(testutils.getByTestId('playlist-add')) + await waitFor(() => { + expect(mockDataProvider.create).toHaveBeenNthCalledWith(1, 'playlist', { + data: { name: 'sample' }, + }) + expect(mockDataProvider.create).toHaveBeenNthCalledWith( + 2, + 'playlistTrack', + { + data: { ids: selectedIds }, + filter: { playlist_id: 'created-id1' }, + } + ) + }) + }) + + it('adds distinct songs to multiple new playlists', async () => { + const testutils = render( + + + + + + ) + + fireEvent.change(document.activeElement, { target: { value: 'sample' } }) + fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }) + fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }) + fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }) + fireEvent.keyDown(document.activeElement, { key: 'Enter' }) + fireEvent.change(document.activeElement, { + target: { value: 'new playlist' }, + }) + fireEvent.keyDown(document.activeElement, { key: 'Enter' }) + await waitFor(() => { + expect(testutils.getByTestId('playlist-add')).not.toBeDisabled() + }) + fireEvent.click(testutils.getByTestId('playlist-add')) + await waitFor(() => { + expect(mockDataProvider.create).toHaveBeenCalledTimes(4) + }) + }) +}) diff --git a/ui/src/dialogs/SelectPlaylistInput.js b/ui/src/dialogs/SelectPlaylistInput.js index 9ee892bf3..fd076ed66 100644 --- a/ui/src/dialogs/SelectPlaylistInput.js +++ b/ui/src/dialogs/SelectPlaylistInput.js @@ -1,5 +1,8 @@ import React from 'react' import TextField from '@material-ui/core/TextField' +import Checkbox from '@material-ui/core/Checkbox' +import CheckBoxIcon from '@material-ui/icons/CheckBox' +import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank' import Autocomplete, { createFilterOptions, } from '@material-ui/lab/Autocomplete' @@ -12,6 +15,7 @@ const filter = createFilterOptions() const useStyles = makeStyles({ root: { width: '100%' }, + checkbox: { marginRight: 8 }, }) export const SelectPlaylistInput = ({ onChange }) => { @@ -29,24 +33,32 @@ export const SelectPlaylistInput = ({ onChange }) => { ids.map((id) => data[id]).filter((option) => isWritable(option.owner)) const handleOnChange = (event, newValue) => { - if (newValue == null) { - onChange({}) - } else if (typeof newValue === 'string') { - onChange({ - name: newValue, + let newState = [] + if (newValue && newValue.length) { + newValue.forEach((playlistObject) => { + if (playlistObject.inputValue) { + newState.push({ + name: playlistObject.inputValue, + }) + } else if (typeof playlistObject === 'string') { + newState.push({ + name: playlistObject, + }) + } else { + newState.push(playlistObject) + } }) - } else if (newValue && newValue.inputValue) { - // Create a new value from the user input - onChange({ - name: newValue.inputValue, - }) - } else { - onChange(newValue) } + onChange(newState) } + const icon = + const checkedIcon = + return ( { const filtered = filter(options, params) @@ -81,7 +93,17 @@ export const SelectPlaylistInput = ({ onChange }) => { // Regular option return option.name }} - renderOption={(option) => option.name} + renderOption={(option, { selected }) => ( + + + {option.name} + + )} className={classes.root} freeSolo renderInput={(params) => ( diff --git a/ui/src/dialogs/SelectPlaylistInput.test.js b/ui/src/dialogs/SelectPlaylistInput.test.js new file mode 100644 index 000000000..751f5d023 --- /dev/null +++ b/ui/src/dialogs/SelectPlaylistInput.test.js @@ -0,0 +1,115 @@ +import * as React from 'react' +import { TestContext } from 'ra-test' +import { DataProviderContext } from 'react-admin' +import { cleanup, fireEvent, render, waitFor } from '@testing-library/react' +import { SelectPlaylistInput } from './SelectPlaylistInput' + +describe('SelectPlaylistInput', () => { + afterEach(cleanup) + const onChangeHandler = jest.fn() + + it('should call the handler with the selections', async () => { + const mockData = [ + { id: 'sample-id1', name: 'sample playlist 1', owner: 'admin' }, + { id: 'sample-id2', name: 'sample playlist 2', owner: 'admin' }, + ] + const mockIndexedData = { + 'sample-id1': { + id: 'sample-id1', + name: 'sample playlist 1', + owner: 'admin', + }, + 'sample-id2': { + id: 'sample-id2', + name: 'sample playlist 2', + owner: 'admin', + }, + } + + const mockDataProvider = { + getList: jest.fn(() => + Promise.resolve({ data: mockData, total: mockData.length }) + ), + } + + render( + + + + + + ) + + await waitFor(() => { + expect(mockDataProvider.getList).toHaveBeenCalledWith('playlist', { + filter: {}, + pagination: { page: 1, perPage: -1 }, + sort: { field: 'name', order: 'ASC' }, + }) + }) + + fireEvent.change(document.activeElement, { target: { value: 'sample' } }) + fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }) + fireEvent.keyDown(document.activeElement, { key: 'Enter' }) + await waitFor(() => { + expect(onChangeHandler).toHaveBeenCalledWith([ + { id: 'sample-id1', name: 'sample playlist 1', owner: 'admin' }, + ]) + }) + + fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }) + fireEvent.keyDown(document.activeElement, { key: 'Enter' }) + await waitFor(() => { + expect(onChangeHandler).toHaveBeenCalledWith([ + { id: 'sample-id1', name: 'sample playlist 1', owner: 'admin' }, + { id: 'sample-id2', name: 'sample playlist 2', owner: 'admin' }, + ]) + }) + + fireEvent.change(document.activeElement, { + target: { value: 'new playlist' }, + }) + fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }) + fireEvent.keyDown(document.activeElement, { key: 'Enter' }) + await waitFor(() => { + expect(onChangeHandler).toHaveBeenCalledWith([ + { id: 'sample-id1', name: 'sample playlist 1', owner: 'admin' }, + { id: 'sample-id2', name: 'sample playlist 2', owner: 'admin' }, + { name: 'new playlist' }, + ]) + }) + + fireEvent.change(document.activeElement, { + target: { value: 'another new playlist' }, + }) + fireEvent.keyDown(document.activeElement, { key: 'Enter' }) + await waitFor(() => { + expect(onChangeHandler).toHaveBeenCalledWith([ + { id: 'sample-id1', name: 'sample playlist 1', owner: 'admin' }, + { id: 'sample-id2', name: 'sample playlist 2', owner: 'admin' }, + { name: 'new playlist' }, + { name: 'another new playlist' }, + ]) + }) + }) +}) diff --git a/ui/src/setupTests.js b/ui/src/setupTests.js index 2eb59b05d..df5f363d4 100644 --- a/ui/src/setupTests.js +++ b/ui/src/setupTests.js @@ -3,3 +3,18 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom/extend-expect' + +class LocalStorageMock { + constructor() { + this.store = {} + } + getItem(key) { + return this.store[key] || null + } + setItem(key, value) { + this.store[key] = String(value) + } +} + +global.localStorage = new LocalStorageMock() +localStorage.setItem('username', 'admin')