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
This commit is contained in:
Yash Jipkate 2021-04-24 04:07:08 +05:30 committed by GitHub
parent d829a63686
commit df57cd6bb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 834 additions and 56 deletions

404
ui/package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 = () => {
<Button onClick={handleClickClose} color="primary">
{translate('ra.action.cancel')}
</Button>
<Button onClick={handleSubmit} color="primary" disabled={!check}>
<Button
onClick={handleSubmit}
color="primary"
disabled={!check}
data-testid="playlist-add"
>
{translate('ra.action.add')}
</Button>
</DialogActions>

View File

@ -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(
<DataProviderContext.Provider value={mockDataProvider}>
<TestContext
initialState={{
addToPlaylistDialog: {
open: true,
duplicateSong: false,
selectedIds: selectedIds,
},
admin: {
ui: { optimistic: false },
resources: {
playlist: {
data: mockIndexedData,
list: {
cachedRequests: {
'{"pagination":{"page":1,"perPage":-1},"sort":{"field":"name","order":"ASC"},"filter":{}}': {
ids: ['sample-id1', 'sample-id2'],
total: 2,
},
},
},
},
},
},
}}
>
<AddToPlaylistDialog />
</TestContext>
</DataProviderContext.Provider>
)
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(
<DataProviderContext.Provider value={mockDataProvider}>
<TestContext
initialState={{
addToPlaylistDialog: {
open: true,
duplicateSong: false,
selectedIds: selectedIds,
},
admin: {
ui: { optimistic: false },
resources: {
playlist: {
data: mockIndexedData,
list: {
cachedRequests: {
'{"pagination":{"page":1,"perPage":-1},"sort":{"field":"name","order":"ASC"},"filter":{}}': {
ids: ['sample-id1', 'sample-id2'],
total: 2,
},
},
},
},
},
},
}}
>
<AddToPlaylistDialog />
</TestContext>
</DataProviderContext.Provider>
)
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(
<DataProviderContext.Provider value={mockDataProvider}>
<TestContext
initialState={{
addToPlaylistDialog: {
open: true,
duplicateSong: false,
selectedIds: selectedIds,
},
admin: {
ui: { optimistic: false },
resources: {
playlist: {
data: mockIndexedData,
list: {
cachedRequests: {
'{"pagination":{"page":1,"perPage":-1},"sort":{"field":"name","order":"ASC"},"filter":{}}': {
ids: ['sample-id1', 'sample-id2'],
total: 2,
},
},
},
},
},
},
}}
>
<AddToPlaylistDialog />
</TestContext>
</DataProviderContext.Provider>
)
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)
})
})
})

View File

@ -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 = <CheckBoxOutlineBlankIcon fontSize="small" />
const checkedIcon = <CheckBoxIcon fontSize="small" />
return (
<Autocomplete
multiple
disableCloseOnSelect
onChange={handleOnChange}
filterOptions={(options, params) => {
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 }) => (
<React.Fragment>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
className={classes.checkbox}
checked={selected}
/>
{option.name}
</React.Fragment>
)}
className={classes.root}
freeSolo
renderInput={(params) => (

View File

@ -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(
<DataProviderContext.Provider value={mockDataProvider}>
<TestContext
initialState={{
addToPlaylistDialog: { open: true, duplicateSong: false },
admin: {
ui: { optimistic: false },
resources: {
playlist: {
data: mockIndexedData,
list: {
cachedRequests: {
'{"pagination":{"page":1,"perPage":-1},"sort":{"field":"name","order":"ASC"},"filter":{}}': {
ids: ['sample-id1', 'sample-id2'],
total: 2,
},
},
},
},
},
},
}}
>
<SelectPlaylistInput onChange={onChangeHandler} />
</TestContext>
</DataProviderContext.Provider>
)
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' },
])
})
})
})

View File

@ -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')