mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-01 19:47:37 +03:00
build(ui): migrate from CRA/Jest to Vite/Vitest (#3311)
* feat: create vite project * feat: it's alive! * feat: `make dev` working! * feat: replace custom serviceWorker with vite plugin * test: replace Jest with Vitest * fix: run prettier * fix: skip eslint for now. * chore: remove ui.old folder * refactor: replace lodash.pick with simple destructuring * fix: eslint errors (wip) * fix: eslint errors (wip) * fix: display-name eslint errors (wip) * fix: no-console eslint errors (wip) * fix: react-refresh/only-export-components eslint errors (wip) * fix: react-refresh/only-export-components eslint errors (wip) * fix: react-refresh/only-export-components eslint errors (wip) * fix: react-refresh/only-export-components eslint errors (wip) * fix: build * fix: pwa manifest * refactor: pwa manifest * refactor: simplify PORT configuration * refactor: rename simple JS files * test: cover playlistUtils * fix: react-image-lightbox * feat(ui): add sourcemaps to help debug issues
This commit is contained in:
parent
dd48a23f92
commit
fcdd30ba8f
212 changed files with 6231 additions and 31060 deletions
2
Makefile
2
Makefile
|
@ -35,7 +35,7 @@ test: ##@Development Run Go tests
|
|||
.PHONY: test
|
||||
|
||||
testall: test ##@Development Run Go and JS tests
|
||||
@(cd ./ui && npm test -- --watchAll=false)
|
||||
@(cd ./ui && npm run test:ci)
|
||||
.PHONY: testall
|
||||
|
||||
lint: ##@Development Lint Go code
|
||||
|
|
5
ui/.eslintignore
Normal file
5
ui/.eslintignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
build/
|
||||
prettier.config.js
|
||||
.eslintrc
|
||||
vite.config.js
|
61
ui/.eslintrc
Normal file
61
ui/.eslintrc
Normal file
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
// "plugin:jsx-a11y/recommended",
|
||||
"eslint-config-prettier",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"warnOnUnsupportedTypeScriptVersion": false
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
},
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"paths": [
|
||||
"src"
|
||||
],
|
||||
"extensions": [
|
||||
".js",
|
||||
".jsx",
|
||||
".ts",
|
||||
".tsx"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": ["react-refresh"],
|
||||
"rules": {
|
||||
"no-console": "error",
|
||||
// "no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ "allowConstantExport": true }
|
||||
],
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/prop-types": "off",
|
||||
},
|
||||
// Fix Vitest
|
||||
"globals": {
|
||||
"describe": "readonly",
|
||||
"it": "readonly",
|
||||
"expect": "readonly",
|
||||
"vi": "readonly",
|
||||
"beforeAll": "readonly",
|
||||
"afterAll": "readonly",
|
||||
"beforeEach": "readonly",
|
||||
"afterEach": "readonly",
|
||||
}
|
||||
}
|
23
ui/.gitignore
vendored
23
ui/.gitignore
vendored
|
@ -1,23 +1,6 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
node_modules
|
||||
.eslintcache
|
||||
|
||||
build/*
|
||||
!build/.gitkeep
|
||||
/coverage/
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
arrowParens: 'always'
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
set -e
|
||||
|
||||
export WORKBOX_DIR=public/3rdparty/workbox
|
||||
|
||||
rm -rf ${WORKBOX_DIR}
|
||||
workbox copyLibraries build/3rdparty/
|
||||
|
||||
mkdir -p public/3rdparty/workbox
|
||||
mv build/3rdparty/workbox-*/workbox-sw.js ${WORKBOX_DIR}
|
||||
mv build/3rdparty/workbox-*/workbox-core.prod.js ${WORKBOX_DIR}
|
||||
mv build/3rdparty/workbox-*/workbox-strategies.prod.js ${WORKBOX_DIR}
|
||||
mv build/3rdparty/workbox-*/workbox-routing.prod.js ${WORKBOX_DIR}
|
||||
mv build/3rdparty/workbox-*/workbox-navigation-preload.prod.js ${WORKBOX_DIR}
|
||||
rm -rf build/3rdparty/workbox-*
|
|
@ -6,11 +6,11 @@
|
|||
name="description"
|
||||
content="Navidrome Music Server - {{.Version}}"
|
||||
/>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="%PUBLIC_URL%/android-chrome-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png">
|
||||
<link rel="mask-icon" href="%PUBLIC_URL%/safari-pinned-tab.svg" color="#5b5fd5">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5b5fd5">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
|
@ -19,16 +19,7 @@
|
|||
manifest.webmanifest provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.webmanifest" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.webmanifest" />
|
||||
<meta property="og:site_name" content="Navidrome">
|
||||
<meta property="og:url" content="{{ .ShareURL }}">
|
||||
<meta property="og:title" content="{{ .ShareDescription }}">
|
||||
|
@ -57,4 +48,5 @@
|
|||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</html>
|
35326
ui/package-lock.json
generated
35326
ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,19 @@
|
|||
{
|
||||
"name": "navidrome-ui",
|
||||
"version": "0.1.0",
|
||||
"name": "ui",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:ci": "vitest --watch=false",
|
||||
"test:coverage": "vitest run --coverage --watch=false",
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint . --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"prettier": "prettier --write ./src",
|
||||
"check-formatting": "prettier -c ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
|
@ -14,7 +26,6 @@
|
|||
"history": "^4.10.1",
|
||||
"inflection": "^1.13.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"navidrome-music-player": "4.25.1",
|
||||
"prop-types": "^15.7.2",
|
||||
|
@ -38,54 +49,30 @@
|
|||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nabla/vite-plugin-eslint": "^2.0.2",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"css-mediaquery": "^0.1.2",
|
||||
"prettier": "3.3.3",
|
||||
"@types/node": "^22.6.1",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"@vitest/coverage-v8": "^2.1.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.4",
|
||||
"jsdom": "^25.0.1",
|
||||
"prettier": "^3.3.3",
|
||||
"ra-test": "^3.19.12",
|
||||
"react-scripts": "5.0.1",
|
||||
"workbox-cli": "^7.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"lint": "eslint --max-warnings 0 src/*.js src/**/*.js",
|
||||
"prettier": "prettier --write src/*.js src/**/*.js",
|
||||
"check-formatting": "prettier -c src/*.js src/**/*.js",
|
||||
"update-workbox": "bin/update-workbox.sh"
|
||||
},
|
||||
"homepage": ".",
|
||||
"proxy": "http://localhost:4633/",
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"src/**/index.js",
|
||||
"src/themes/*.js"
|
||||
],
|
||||
"rules": {
|
||||
"import/no-anonymous-default-export": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^5.3.5",
|
||||
"vite-plugin-pwa": "^0.20.5",
|
||||
"vitest": "^2.1.1"
|
||||
}
|
||||
}
|
||||
|
|
5
ui/prettier.config.js
Normal file
5
ui/prettier.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
arrowParens: "always",
|
||||
};
|
|
@ -1,2 +0,0 @@
|
|||
this.workbox=this.workbox||{},this.workbox.core=function(t){"use strict";try{self["workbox:core:6.2.4"]&&_()}catch(t){}const e=(t,...e)=>{let n=t;return e.length>0&&(n+=" :: "+JSON.stringify(e)),n};class n extends Error{constructor(t,n){super(e(t,n)),this.name=t,this.details=n}}const r=new Set;const o={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},s=t=>[o.prefix,t,o.suffix].filter((t=>t&&t.length>0)).join("-"),i={updateDetails:t=>{(t=>{for(const e of Object.keys(o))t(e)})((e=>{"string"==typeof t[e]&&(o[e]=t[e])}))},getGoogleAnalyticsName:t=>t||s(o.googleAnalytics),getPrecacheName:t=>t||s(o.precache),getPrefix:()=>o.prefix,getRuntimeName:t=>t||s(o.runtime),getSuffix:()=>o.suffix};function c(t,e){const n=new URL(t);for(const t of e)n.searchParams.delete(t);return n.href}let a,u;function f(){if(void 0===u){const t=new Response("");if("body"in t)try{new Response(t.body),u=!0}catch(t){u=!1}u=!1}return u}function l(t){return new Promise((e=>setTimeout(e,t)))}var g=Object.freeze({__proto__:null,assert:null,cacheMatchIgnoreParams:async function(t,e,n,r){const o=c(e.url,n);if(e.url===o)return t.match(e,r);const s=Object.assign(Object.assign({},r),{ignoreSearch:!0}),i=await t.keys(e,s);for(const e of i){if(o===c(e.url,n))return t.match(e,r)}},cacheNames:i,canConstructReadableStream:function(){if(void 0===a)try{new ReadableStream({start(){}}),a=!0}catch(t){a=!1}return a},canConstructResponseFromBodyStream:f,dontWaitFor:function(t){t.then((()=>{}))},Deferred:class{constructor(){this.promise=new Promise(((t,e)=>{this.resolve=t,this.reject=e}))}},executeQuotaErrorCallbacks:async function(){for(const t of r)await t()},getFriendlyURL:t=>new URL(String(t),location.href).href.replace(new RegExp("^"+location.origin),""),logger:null,resultingClientExists:async function(t){if(!t)return;let e=await self.clients.matchAll({type:"window"});const n=new Set(e.map((t=>t.id)));let r;const o=performance.now();for(;performance.now()-o<2e3&&(e=await self.clients.matchAll({type:"window"}),r=e.find((e=>t?e.id===t:!n.has(e.id))),!r);)await l(100);return r},timeout:l,waitUntil:function(t,e){const n=e();return t.waitUntil(n),n},WorkboxError:n});const w={get googleAnalytics(){return i.getGoogleAnalyticsName()},get precache(){return i.getPrecacheName()},get prefix(){return i.getPrefix()},get runtime(){return i.getRuntimeName()},get suffix(){return i.getSuffix()}};return t._private=g,t.cacheNames=w,t.clientsClaim=function(){self.addEventListener("activate",(()=>self.clients.claim()))},t.copyResponse=async function(t,e){let r=null;if(t.url){r=new URL(t.url).origin}if(r!==self.location.origin)throw new n("cross-origin-copy-response",{origin:r});const o=t.clone(),s={headers:new Headers(o.headers),status:o.status,statusText:o.statusText},i=e?e(s):s,c=f()?o.body:await o.blob();return new Response(c,i)},t.registerQuotaErrorCallback=function(t){r.add(t)},t.setCacheNameDetails=function(t){i.updateDetails(t)},t.skipWaiting=function(){self.skipWaiting()},t}({});
|
||||
//# sourceMappingURL=workbox-core.prod.js.map
|
|
@ -1,2 +0,0 @@
|
|||
this.workbox=this.workbox||{},this.workbox.navigationPreload=function(t){"use strict";try{self["workbox:navigation-preload:6.2.4"]&&_()}catch(t){}function e(){return Boolean(self.registration&&self.registration.navigationPreload)}return t.disable=function(){e()&&self.addEventListener("activate",(t=>{t.waitUntil(self.registration.navigationPreload.disable().then((()=>{})))}))},t.enable=function(t){e()&&self.addEventListener("activate",(e=>{e.waitUntil(self.registration.navigationPreload.enable().then((()=>{t&&self.registration.navigationPreload.setHeaderValue(t)})))}))},t.isSupported=e,t}({});
|
||||
//# sourceMappingURL=workbox-navigation-preload.prod.js.map
|
|
@ -1,2 +0,0 @@
|
|||
this.workbox=this.workbox||{},this.workbox.routing=function(t,e){"use strict";try{self["workbox:routing:6.2.4"]&&_()}catch(t){}const s=t=>t&&"object"==typeof t?t:{handle:t};class r{constructor(t,e,r="GET"){this.handler=s(e),this.match=t,this.method=r}setCatchHandler(t){this.catchHandler=s(t)}}class n extends r{constructor(t,e,s){super((({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)}),e,s)}}class i{constructor(){this.ut=new Map,this.ft=new Map}get routes(){return this.ut}addFetchListener(){self.addEventListener("fetch",(t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map((e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})})));t.waitUntil(s),t.ports&&t.ports[0]&&s.then((()=>t.ports[0].postMessage(!0)))}}))}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const r=s.origin===location.origin,{params:n,route:i}=this.findMatchingRoute({event:e,request:t,sameOrigin:r,url:s});let o=i&&i.handler;const u=t.method;if(!o&&this.ft.has(u)&&(o=this.ft.get(u)),!o)return;let c;try{c=o.handle({url:s,request:t,event:e,params:n})}catch(t){c=Promise.reject(t)}const a=i&&i.catchHandler;return c instanceof Promise&&(this.lt||a)&&(c=c.catch((async r=>{if(a)try{return await a.handle({url:s,request:t,event:e,params:n})}catch(t){t instanceof Error&&(r=t)}if(this.lt)return this.lt.handle({url:s,request:t,event:e});throw r}))),c}findMatchingRoute({url:t,sameOrigin:e,request:s,event:r}){const n=this.ut.get(s.method)||[];for(const i of n){let n;const o=i.match({url:t,sameOrigin:e,request:s,event:r});if(o)return n=o,(Array.isArray(n)&&0===n.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(n=void 0),{route:i,params:n}}return{}}setDefaultHandler(t,e="GET"){this.ft.set(e,s(t))}setCatchHandler(t){this.lt=s(t)}registerRoute(t){this.ut.has(t.method)||this.ut.set(t.method,[]),this.ut.get(t.method).push(t)}unregisterRoute(t){if(!this.ut.has(t.method))throw new e.WorkboxError("unregister-route-but-not-found-with-method",{method:t.method});const s=this.ut.get(t.method).indexOf(t);if(!(s>-1))throw new e.WorkboxError("unregister-route-route-not-registered");this.ut.get(t.method).splice(s,1)}}let o;const u=()=>(o||(o=new i,o.addFetchListener(),o.addCacheListener()),o);return t.NavigationRoute=class extends r{constructor(t,{allowlist:e=[/./],denylist:s=[]}={}){super((t=>this.dt(t)),t),this.wt=e,this.gt=s}dt({url:t,request:e}){if(e&&"navigate"!==e.mode)return!1;const s=t.pathname+t.search;for(const t of this.gt)if(t.test(s))return!1;return!!this.wt.some((t=>t.test(s)))}},t.RegExpRoute=n,t.Route=r,t.Router=i,t.registerRoute=function(t,s,i){let o;if("string"==typeof t){const e=new URL(t,location.href);o=new r((({url:t})=>t.href===e.href),s,i)}else if(t instanceof RegExp)o=new n(t,s,i);else if("function"==typeof t)o=new r(t,s,i);else{if(!(t instanceof r))throw new e.WorkboxError("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});o=t}return u().registerRoute(o),o},t.setCatchHandler=function(t){u().setCatchHandler(t)},t.setDefaultHandler=function(t){u().setDefaultHandler(t)},t}({},workbox.core._private);
|
||||
//# sourceMappingURL=workbox-routing.prod.js.map
|
File diff suppressed because one or more lines are too long
2
ui/public/3rdparty/workbox/workbox-sw.js
vendored
2
ui/public/3rdparty/workbox/workbox-sw.js
vendored
|
@ -1,2 +0,0 @@
|
|||
!function(){"use strict";try{self["workbox:sw:6.3.0"]&&_()}catch(t){}const t={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams",recipes:"recipes"};self.workbox=new class{constructor(){return this.v={},this.Pt={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.$t=this.Pt.debug?"dev":"prod",this.Ct=!1,new Proxy(this,{get(e,s){if(e[s])return e[s];const o=t[s];return o&&e.loadModule("workbox-"+o),e[s]}})}setConfig(t={}){if(this.Ct)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.Pt,t),this.$t=this.Pt.debug?"dev":"prod"}loadModule(t){const e=this.jt(t);try{importScripts(e),this.Ct=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}jt(t){if(this.Pt.modulePathCb)return this.Pt.modulePathCb(t,this.Pt.debug);let e=["https://storage.googleapis.com/workbox-cdn/releases/6.3.0"];const s=`${t}.${this.$t}.js`,o=this.Pt.modulePathPrefix;return o&&(e=o.split("/"),""===e[e.length-1]&&e.splice(e.length-1,1)),e.push(s),e.join("/")}}}();
|
||||
//# sourceMappingURL=workbox-sw.js.map
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"name": "Navidrome",
|
||||
"short_name": "Navidrome",
|
||||
"description": "Navidrome, an open source web-based music collection server and streamer",
|
||||
"categories": ["music", "entertainment"],
|
||||
"display": "standalone",
|
||||
"start_url": "./",
|
||||
"background_color": "white",
|
||||
"theme_color": "blue",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
// documentation: https://developers.google.com/web/tools/workbox/modules/workbox-sw
|
||||
importScripts('3rdparty/workbox/workbox-sw.js')
|
||||
|
||||
workbox.setConfig({
|
||||
modulePathPrefix: '3rdparty/workbox/',
|
||||
debug: false,
|
||||
})
|
||||
|
||||
workbox.loadModule('workbox-core');
|
||||
workbox.loadModule('workbox-strategies');
|
||||
workbox.loadModule('workbox-routing');
|
||||
workbox.loadModule('workbox-navigation-preload');
|
||||
|
||||
workbox.core.clientsClaim();
|
||||
self.skipWaiting();
|
||||
|
||||
addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const CACHE_NAME = 'offline-html';
|
||||
// This assumes /offline.html is a URL for your self-contained
|
||||
// (no external images or styles) offline page.
|
||||
const FALLBACK_HTML_URL = './offline.html';
|
||||
// Populate the cache with the offline HTML page when the
|
||||
// service worker is installed.
|
||||
self.addEventListener('install', async (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => cache.add(FALLBACK_HTML_URL))
|
||||
);
|
||||
});
|
||||
|
||||
const networkOnly = new workbox.strategies.NetworkOnly();
|
||||
const navigationHandler = async (params) => {
|
||||
try {
|
||||
// Attempt a network request.
|
||||
return await networkOnly.handle(params);
|
||||
} catch (error) {
|
||||
// If it fails, return the cached HTML.
|
||||
return caches.match(FALLBACK_HTML_URL, {
|
||||
cacheName: CACHE_NAME,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Register this strategy to handle all navigations.
|
||||
workbox.routing.registerRoute(
|
||||
new workbox.routing.NavigationRoute(navigationHandler)
|
||||
);
|
|
@ -1,10 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><title>Navidrome</title></head>
|
||||
<body style="margin:0">
|
||||
<p id="errorMessageDescription" style="text-align:center;font-size:21px;font-family:arial;margin-top:28px">
|
||||
It looks like we are having trouble connecting.
|
||||
<br/>
|
||||
Please check your internet connection and try again.</p>
|
||||
</body>
|
||||
</html>
|
|
@ -74,7 +74,7 @@ const App = () => (
|
|||
|
||||
const Admin = (props) => {
|
||||
useChangeThemeColor()
|
||||
|
||||
/* eslint-disable react/jsx-key */
|
||||
return (
|
||||
<RAAdmin
|
||||
disableTelemetry
|
||||
|
@ -125,6 +125,7 @@ const Admin = (props) => {
|
|||
]}
|
||||
</RAAdmin>
|
||||
)
|
||||
/* eslint-enable react/jsx-key */
|
||||
}
|
||||
|
||||
const AppWithHotkeys = () => {
|
|
@ -106,7 +106,7 @@ const useStyles = makeStyles(
|
|||
},
|
||||
)
|
||||
|
||||
export const useGetHandleGenreClick = (width) => {
|
||||
const useGetHandleGenreClick = (width) => {
|
||||
const [perPage] = useAlbumsPerPage(width)
|
||||
|
||||
return (id) => {
|
||||
|
@ -244,6 +244,7 @@ const AlbumDetails = (props) => {
|
|||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('error on album page', e)
|
||||
})
|
||||
}, [record])
|
|
@ -210,4 +210,6 @@ const AlbumGridView = ({ albumListType, loaded, loading, ...props }) => {
|
|||
return hide ? <Loading /> : <LoadedAlbumGrid {...props} />
|
||||
}
|
||||
|
||||
export default withWidth()(AlbumGridView)
|
||||
const AlbumGridViewWithWidth = withWidth()(AlbumGridView)
|
||||
|
||||
export default AlbumGridViewWithWidth
|
|
@ -152,4 +152,6 @@ const AlbumList = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default withWidth()(AlbumList)
|
||||
const AlbumListWithWidth = withWidth()(AlbumList)
|
||||
|
||||
export default AlbumListWithWidth
|
|
@ -67,6 +67,8 @@ const AlbumViewToggler = React.forwardRef(
|
|||
},
|
||||
)
|
||||
|
||||
AlbumViewToggler.displayName = 'AlbumViewToggler'
|
||||
|
||||
const AlbumListActions = ({
|
||||
currentSort,
|
||||
className,
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
BulkActionsToolbar,
|
||||
ListToolbar,
|
||||
TextField,
|
||||
NumberField,
|
||||
useVersion,
|
||||
useListContext,
|
||||
FunctionField,
|
||||
ListToolbar,
|
||||
NumberField,
|
||||
TextField,
|
||||
useListContext,
|
||||
useVersion,
|
||||
} from 'react-admin'
|
||||
import clsx from 'clsx'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
@ -15,22 +15,23 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
|
||||
import { playTracks } from '../actions'
|
||||
import {
|
||||
ArtistLinkField,
|
||||
DateField,
|
||||
DurationField,
|
||||
QualityInfo,
|
||||
RatingField,
|
||||
SizeField,
|
||||
SongBulkActions,
|
||||
SongContextMenu,
|
||||
SongDatagrid,
|
||||
SongInfo,
|
||||
SongTitleField,
|
||||
RatingField,
|
||||
QualityInfo,
|
||||
useSelectedFields,
|
||||
useResourceRefresh,
|
||||
DateField,
|
||||
SizeField,
|
||||
ArtistLinkField,
|
||||
useSelectedFields,
|
||||
} from '../common'
|
||||
import config from '../config'
|
||||
import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
|
||||
import { removeAlbumCommentsFromSongs } from './utils.js'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
(theme) => ({
|
||||
|
@ -193,14 +194,6 @@ const AlbumSongs = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
export const removeAlbumCommentsFromSongs = ({ album, data }) => {
|
||||
if (album?.comment && data) {
|
||||
Object.values(data).forEach((song) => {
|
||||
song.comment = ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const SanitizedAlbumSongs = (props) => {
|
||||
removeAlbumCommentsFromSongs(props)
|
||||
const { loaded, loading, total, ...rest } = useListContext(props)
|
7
ui/src/album/utils.js
Normal file
7
ui/src/album/utils.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const removeAlbumCommentsFromSongs = ({ album, data }) => {
|
||||
if (album?.comment && data) {
|
||||
Object.values(data).forEach((song) => {
|
||||
song.comment = ''
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { removeAlbumCommentsFromSongs } from './AlbumSongs'
|
||||
import { removeAlbumCommentsFromSongs } from './utils.js'
|
||||
|
||||
describe('removeAlbumCommentsFromSongs', () => {
|
||||
const data = { 1: { comment: 'one' }, 2: { comment: 'two' } }
|
|
@ -179,4 +179,6 @@ const ArtistList = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default withWidth()(ArtistList)
|
||||
const ArtistListWithWidth = withWidth()(ArtistList)
|
||||
|
||||
export default ArtistListWithWidth
|
|
@ -31,6 +31,7 @@ const ArtistDetails = (props) => {
|
|||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('error on artist page', e)
|
||||
})
|
||||
}, [record.id])
|
|
@ -66,4 +66,6 @@ const AudioTitle = React.memo(({ audioInfo, gainInfo, isMobile }) => {
|
|||
)
|
||||
})
|
||||
|
||||
AudioTitle.displayName = 'AudioTitle'
|
||||
|
||||
export default AudioTitle
|
|
@ -257,6 +257,7 @@ const Player = () => {
|
|||
dispatch(currentPlaying(info))
|
||||
dataProvider
|
||||
.getOne('keepalive', { id: info.trackId })
|
||||
// eslint-disable-next-line no-console
|
||||
.catch((e) => console.log('Keepalive error:', e))
|
||||
},
|
||||
[dispatch, dataProvider],
|
|
@ -8,6 +8,7 @@ if (config.auth) {
|
|||
try {
|
||||
storeAuthenticationInfo(config.auth)
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,17 +2,7 @@ import React from 'react'
|
|||
import PropTypes from 'prop-types'
|
||||
import { Link } from 'react-admin'
|
||||
import { withWidth } from '@material-ui/core'
|
||||
import { useAlbumsPerPage } from './index'
|
||||
import config from '../config'
|
||||
|
||||
export const useGetHandleArtistClick = (width) => {
|
||||
const [perPage] = useAlbumsPerPage(width)
|
||||
return (id) => {
|
||||
return config.devShowArtistPage && id !== config.variousArtistsId
|
||||
? `/artist/${id}/show`
|
||||
: `/album?filter={"artist_id":"${id}"}&order=ASC&sort=max_year&displayedFilters={"compilation":true}&perPage=${perPage}`
|
||||
}
|
||||
}
|
||||
import { useGetHandleArtistClick } from './useGetHandleArtistClick'
|
||||
|
||||
export const ArtistLinkField = withWidth()(({
|
||||
record,
|
|
@ -8,6 +8,7 @@ export const DurationField = ({ source, ...rest }) => {
|
|||
try {
|
||||
return <span>{formatDuration(record[source])}</span>
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error in DurationField! Record:', record)
|
||||
return <span>00:00</span>
|
||||
}
|
|
@ -46,6 +46,8 @@ export const MultiLineTextField = memo(
|
|||
},
|
||||
)
|
||||
|
||||
MultiLineTextField.displayName = 'MultiLineTextField'
|
||||
|
||||
MultiLineTextField.defaultProps = {
|
||||
addLabel: true,
|
||||
firstLine: 0,
|
|
@ -1,20 +1,7 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useRecordContext } from 'react-admin'
|
||||
|
||||
export const formatRange = (record, source) => {
|
||||
const nameCapitalized = source.charAt(0).toUpperCase() + source.slice(1)
|
||||
const min = record[`min${nameCapitalized}`]
|
||||
const max = record[`max${nameCapitalized}`]
|
||||
let range = []
|
||||
if (min) {
|
||||
range.push(min)
|
||||
}
|
||||
if (max && max !== min) {
|
||||
range.push(max)
|
||||
}
|
||||
return range.join('-')
|
||||
}
|
||||
import { formatRange } from './formatRange'
|
||||
|
||||
export const RangeField = ({ className, source, ...rest }) => {
|
||||
const record = useRecordContext(rest)
|
|
@ -100,6 +100,8 @@ const ReleaseRow = forwardRef(
|
|||
},
|
||||
)
|
||||
|
||||
ReleaseRow.displayName = 'ReleaseRow'
|
||||
|
||||
const DiscSubtitleRow = forwardRef(
|
||||
({ record, onClick, colSpan, contextAlwaysVisible }, ref) => {
|
||||
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
||||
|
@ -146,6 +148,8 @@ const DiscSubtitleRow = forwardRef(
|
|||
},
|
||||
)
|
||||
|
||||
DiscSubtitleRow.displayName = 'DiscSubtitleRow'
|
||||
|
||||
export const SongDatagridRow = ({
|
||||
record,
|
||||
children,
|
12
ui/src/common/Writable.jsx
Normal file
12
ui/src/common/Writable.jsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Children, cloneElement, isValidElement } from 'react'
|
||||
import { isWritable } from './playlistUtils.js'
|
||||
|
||||
export const Writable = (props) => {
|
||||
const { record = {}, children } = props
|
||||
if (isWritable(record.ownerId)) {
|
||||
return Children.map(children, (child) =>
|
||||
isValidElement(child) ? cloneElement(child, props) : child,
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
13
ui/src/common/formatRange.js
Normal file
13
ui/src/common/formatRange.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export const formatRange = (record, source) => {
|
||||
const nameCapitalized = source.charAt(0).toUpperCase() + source.slice(1)
|
||||
const min = record[`min${nameCapitalized}`]
|
||||
const max = record[`max${nameCapitalized}`]
|
||||
let range = []
|
||||
if (min) {
|
||||
range.push(min)
|
||||
}
|
||||
if (max && max !== min) {
|
||||
range.push(max)
|
||||
}
|
||||
return range.join('-')
|
||||
}
|
|
@ -25,6 +25,7 @@ export * from './LoveButton'
|
|||
export * from './Title'
|
||||
export * from './SongBulkActions'
|
||||
export * from './useAlbumsPerPage'
|
||||
export * from './useGetHandleArtistClick'
|
||||
export * from './useInterval'
|
||||
export * from './useResourceRefresh'
|
||||
export * from './useToggleLove'
|
||||
|
@ -37,3 +38,5 @@ export * from './useRating'
|
|||
export * from './useSelectedFields'
|
||||
export * from './ToggleFieldsMenu'
|
||||
export * from './QualityInfo'
|
||||
export * from './formatRange.js'
|
||||
export * from './playlistUtils.js'
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { cloneElement, Children, isValidElement } from 'react'
|
||||
|
||||
export const isWritable = (ownerId) => {
|
||||
return (
|
||||
localStorage.getItem('userId') === ownerId ||
|
||||
|
@ -11,16 +9,6 @@ export const isReadOnly = (ownerId) => {
|
|||
return !isWritable(ownerId)
|
||||
}
|
||||
|
||||
export const Writable = (props) => {
|
||||
const { record = {}, children } = props
|
||||
if (isWritable(record.ownerId)) {
|
||||
return Children.map(children, (child) =>
|
||||
isValidElement(child) ? cloneElement(child, props) : child,
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const isSmartPlaylist = (pls) => !!pls.rules
|
||||
|
||||
export const canChangeTracks = (pls) =>
|
78
ui/src/common/playlistUtils.test.js
Normal file
78
ui/src/common/playlistUtils.test.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
import {
|
||||
isWritable,
|
||||
isReadOnly,
|
||||
isSmartPlaylist,
|
||||
canChangeTracks,
|
||||
} from './playlistUtils'
|
||||
|
||||
describe('playlistUtils', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
describe('isWritable', () => {
|
||||
it('returns true if user is the owner', () => {
|
||||
localStorage.setItem('userId', 'user1')
|
||||
expect(isWritable('user1')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true if user is an admin', () => {
|
||||
localStorage.setItem('role', 'admin')
|
||||
expect(isWritable('user1')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false if user is not the owner and not an admin', () => {
|
||||
localStorage.setItem('userId', 'user2')
|
||||
expect(isWritable('user1')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isReadOnly', () => {
|
||||
it('returns true if user is not the owner and not an admin', () => {
|
||||
localStorage.setItem('userId', 'user2')
|
||||
expect(isReadOnly('user1')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false if user is the owner', () => {
|
||||
localStorage.setItem('userId', 'user1')
|
||||
expect(isReadOnly('user1')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false if user is an admin', () => {
|
||||
localStorage.setItem('role', 'admin')
|
||||
expect(isReadOnly('user1')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isSmartPlaylist', () => {
|
||||
it('returns true if playlist has rules', () => {
|
||||
const playlist = { rules: [] }
|
||||
expect(isSmartPlaylist(playlist)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false if playlist does not have rules', () => {
|
||||
const playlist = {}
|
||||
expect(isSmartPlaylist(playlist)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('canChangeTracks', () => {
|
||||
it('returns true if user is the owner and playlist is not smart', () => {
|
||||
localStorage.setItem('userId', 'user1')
|
||||
const playlist = { ownerId: 'user1' }
|
||||
expect(canChangeTracks(playlist)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false if user is not the owner', () => {
|
||||
localStorage.setItem('userId', 'user2')
|
||||
const playlist = { ownerId: 'user1' }
|
||||
expect(canChangeTracks(playlist)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false if playlist is smart', () => {
|
||||
localStorage.setItem('userId', 'user1')
|
||||
const playlist = { ownerId: 'user1', rules: [] }
|
||||
expect(canChangeTracks(playlist)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
11
ui/src/common/useGetHandleArtistClick.jsx
Normal file
11
ui/src/common/useGetHandleArtistClick.jsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { useAlbumsPerPage } from './useAlbumsPerPage'
|
||||
import config from '../config.js'
|
||||
|
||||
export const useGetHandleArtistClick = (width) => {
|
||||
const [perPage] = useAlbumsPerPage(width)
|
||||
return (id) => {
|
||||
return config.devShowArtistPage && id !== config.variousArtistsId
|
||||
? `/artist/${id}/show`
|
||||
: `/album?filter={"artist_id":"${id}"}&order=ASC&sort=max_year&displayedFilters={"compilation":true}&perPage=${perPage}`
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ export const useRating = (resource, record) => {
|
|||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error encountered: ' + e)
|
||||
})
|
||||
}, [dataProvider, record, resource])
|
||||
|
@ -35,6 +36,7 @@ export const useRating = (resource, record) => {
|
|||
.setRating(id, val)
|
||||
.then(refreshRating)
|
||||
.catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error setting star rating: ', e)
|
||||
notify('ra.page.error', 'warning')
|
||||
if (mountedRef.current) {
|
|
@ -1,47 +1,57 @@
|
|||
import { vi } from 'vitest'
|
||||
import * as React from 'react'
|
||||
import * as Redux from 'react-redux'
|
||||
import * as RA from 'react-admin'
|
||||
import { useResourceRefresh } from './useResourceRefresh'
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useState: jest.fn(),
|
||||
}))
|
||||
vi.mock('react', async () => {
|
||||
const actual = await vi.importActual('react')
|
||||
return {
|
||||
...actual,
|
||||
useState: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}))
|
||||
vi.mock('react-redux', async () => {
|
||||
const actual = await vi.importActual('react-redux')
|
||||
return {
|
||||
...actual,
|
||||
useSelector: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('react-admin', () => ({
|
||||
...jest.requireActual('react-admin'),
|
||||
useRefresh: jest.fn(),
|
||||
useDataProvider: jest.fn(),
|
||||
}))
|
||||
vi.mock('react-admin', async () => {
|
||||
const actual = await vi.importActual('react-admin')
|
||||
return {
|
||||
...actual,
|
||||
useRefresh: vi.fn(),
|
||||
useDataProvider: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('useResourceRefresh', () => {
|
||||
const setState = jest.fn()
|
||||
const setState = vi.fn()
|
||||
const useStateMock = (initState) => [initState, setState]
|
||||
const refresh = jest.fn()
|
||||
const refresh = vi.fn()
|
||||
const useRefreshMock = () => refresh
|
||||
const getMany = jest.fn()
|
||||
const getMany = vi.fn()
|
||||
const useDataProviderMock = () => ({ getMany })
|
||||
let lastTime
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(React, 'useState').mockImplementation(useStateMock)
|
||||
jest.spyOn(RA, 'useRefresh').mockImplementation(useRefreshMock)
|
||||
jest.spyOn(RA, 'useDataProvider').mockImplementation(useDataProviderMock)
|
||||
vi.spyOn(React, 'useState').mockImplementation(useStateMock)
|
||||
vi.spyOn(RA, 'useRefresh').mockImplementation(useRefreshMock)
|
||||
vi.spyOn(RA, 'useDataProvider').mockImplementation(useDataProviderMock)
|
||||
lastTime = new Date(new Date().valueOf() + 1000)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('stores last time checked, to avoid redundant runs', () => {
|
||||
const useSelectorMock = () => ({ lastReceived: lastTime })
|
||||
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
|
||||
useResourceRefresh()
|
||||
|
||||
|
@ -49,9 +59,9 @@ describe('useResourceRefresh', () => {
|
|||
})
|
||||
|
||||
it("does not run again if lastTime didn't change", () => {
|
||||
jest.spyOn(React, 'useState').mockImplementation(() => [lastTime, setState])
|
||||
vi.spyOn(React, 'useState').mockImplementation(() => [lastTime, setState])
|
||||
const useSelectorMock = () => ({ lastReceived: lastTime })
|
||||
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
|
||||
useResourceRefresh()
|
||||
|
||||
|
@ -64,7 +74,7 @@ describe('useResourceRefresh', () => {
|
|||
lastReceived: lastTime,
|
||||
resources: { '*': '*' },
|
||||
})
|
||||
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
|
||||
useResourceRefresh()
|
||||
|
||||
|
@ -77,7 +87,7 @@ describe('useResourceRefresh', () => {
|
|||
lastReceived: lastTime,
|
||||
resources: { album: ['*'] },
|
||||
})
|
||||
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
|
||||
useResourceRefresh()
|
||||
|
||||
|
@ -90,7 +100,7 @@ describe('useResourceRefresh', () => {
|
|||
lastReceived: lastTime,
|
||||
resources: { album: ['al-1', 'al-2'], song: ['sg-1', 'sg-2'] },
|
||||
})
|
||||
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
|
||||
useResourceRefresh()
|
||||
|
||||
|
@ -107,7 +117,7 @@ describe('useResourceRefresh', () => {
|
|||
lastReceived: lastTime,
|
||||
resources: { '*': '*' },
|
||||
})
|
||||
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
|
||||
useResourceRefresh('album')
|
||||
|
||||
|
@ -120,7 +130,7 @@ describe('useResourceRefresh', () => {
|
|||
lastReceived: lastTime,
|
||||
resources: { album: ['al-1', 'al-2'], song: ['sg-1', 'sg-2'] },
|
||||
})
|
||||
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||||
|
||||
useResourceRefresh('song')
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ export const useToggleLove = (resource, record = {}) => {
|
|||
toggle(record.id)
|
||||
.then(refreshRecord)
|
||||
.catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error toggling love: ', e)
|
||||
notify('ra.page.error', 'warning')
|
||||
if (mountedRef.current) {
|
|
@ -10,6 +10,7 @@ export function useTraceUpdate(props) {
|
|||
return ps
|
||||
}, {})
|
||||
if (Object.keys(changedProps).length > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Changed props:', changedProps)
|
||||
}
|
||||
prev.current = props
|
|
@ -6,14 +6,14 @@ const dataProvider = jsonServerProvider(REST_URL, httpClient)
|
|||
|
||||
const mapResource = (resource, params) => {
|
||||
switch (resource) {
|
||||
case 'playlistTrack':
|
||||
case 'playlistTrack': {
|
||||
// /api/playlistTrack?playlist_id=123 => /api/playlist/123/tracks
|
||||
let plsId = '0'
|
||||
if (params.filter) {
|
||||
plsId = params.filter.playlist_id
|
||||
}
|
||||
return [`playlist/${plsId}/tracks`, params]
|
||||
|
||||
}
|
||||
default:
|
||||
return [resource, params]
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue