mirror of
https://github.com/navidrome/navidrome.git
synced 2025-06-10 20:32:27 +03:00
Upgrade Web UI to Create-React-App 4 and React 17 (#1105)
* Upgrade to CRA 4.0.3 * Try to fix tests. No lucky * Fix new ESLint errors * Fix JS tests and remove unwanted dependency. (#1106) * Fix tests * Fix lint * Remove React v16 workaround (fixed in v17) * Force eslint to break on warnings * Lint now needs to be called explicitly in the pipeline Co-authored-by: Yash Jipkate <34203227+YashJipkate@users.noreply.github.com>
This commit is contained in:
parent
d9f268266c
commit
5631493cc4
4
.github/workflows/pipeline.yml
vendored
4
.github/workflows/pipeline.yml
vendored
@ -85,10 +85,10 @@ jobs:
|
|||||||
cd ui
|
cd ui
|
||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
- name: npm check-formatting
|
- name: npm lint
|
||||||
run: |
|
run: |
|
||||||
cd ui
|
cd ui
|
||||||
npm run check-formatting
|
npm run check-formatting && npm run lint
|
||||||
|
|
||||||
- name: npm test
|
- name: npm test
|
||||||
run: |
|
run: |
|
||||||
|
12842
ui/package-lock.json
generated
12842
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,24 +17,25 @@
|
|||||||
"lodash.pick": "^4.4.0",
|
"lodash.pick": "^4.4.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"ra-data-json-server": "^3.15.2",
|
"ra-data-json-server": "^3.15.1",
|
||||||
"ra-i18n-polyglot": "^3.15.2",
|
"ra-i18n-polyglot": "^3.15.1",
|
||||||
"react": "^16.14.0",
|
"react": "^17.0.2",
|
||||||
"react-admin": "^3.15.2",
|
"react-admin": "^3.15.1",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^17.0.2",
|
||||||
"react-drag-listview": "^0.1.8",
|
"react-drag-listview": "^0.1.8",
|
||||||
"react-ga": "^3.3.0",
|
"react-ga": "^3.3.0",
|
||||||
"react-hotkeys": "^2.0.0",
|
"react-hotkeys": "^2.0.0",
|
||||||
"react-icons": "^4.2.0",
|
"react-icons": "^4.2.0",
|
||||||
"react-image-lightbox": "^5.1.1",
|
"react-image-lightbox": "^5.1.1",
|
||||||
"react-jinke-music-player": "^4.24.1",
|
"react-jinke-music-player": "^4.24.0",
|
||||||
"react-measure": "^2.5.2",
|
"react-measure": "^2.5.2",
|
||||||
"react-redux": "^7.2.4",
|
"react-redux": "^7.2.4",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^3.4.3",
|
"react-scripts": "^4.0.3",
|
||||||
"redux": "^4.1.0",
|
"redux": "^4.1.0",
|
||||||
"redux-saga": "^1.1.3",
|
"redux-saga": "^1.1.3",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2",
|
||||||
|
"web-vitals": "^0.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^5.12.0",
|
"@testing-library/jest-dom": "^5.12.0",
|
||||||
@ -42,15 +43,14 @@
|
|||||||
"@testing-library/react-hooks": "^5.1.2",
|
"@testing-library/react-hooks": "^5.1.2",
|
||||||
"@testing-library/user-event": "^13.1.8",
|
"@testing-library/user-event": "^13.1.8",
|
||||||
"css-mediaquery": "^0.1.2",
|
"css-mediaquery": "^0.1.2",
|
||||||
"jest-environment-jsdom-sixteen": "^2.0.0",
|
|
||||||
"prettier": "2.3.0",
|
"prettier": "2.3.0",
|
||||||
"ra-test": "^3.15.2"
|
"ra-test": "^3.15.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test --env=jest-environment-jsdom-sixteen",
|
"test": "react-scripts test",
|
||||||
"lint": "eslint -c node_modules/eslint-config-react-app/index.js src/**/*.js",
|
"lint": "eslint --max-warnings 0 src/**/*.js",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"prettier": "prettier --write src/*.js src/**/*.js",
|
"prettier": "prettier --write src/*.js src/**/*.js",
|
||||||
"check-formatting": "prettier -c src/*.js src/**/*.js"
|
"check-formatting": "prettier -c src/*.js src/**/*.js"
|
||||||
@ -58,7 +58,21 @@
|
|||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"proxy": "http://localhost:4633/",
|
"proxy": "http://localhost:4633/",
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"src/**/index.js",
|
||||||
|
"src/themes/*.js"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"import/no-anonymous-default-export": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
@ -33,11 +33,6 @@
|
|||||||
<script>
|
<script>
|
||||||
window.__APP_CONFIG__ = "{{.AppConfig}}"
|
window.__APP_CONFIG__ = "{{.AppConfig}}"
|
||||||
</script>
|
</script>
|
||||||
<!-- Issue workaround for React v16. -->
|
|
||||||
<script>
|
|
||||||
// See https://github.com/facebook/react/issues/20829#issuecomment-802088260
|
|
||||||
if (!crossOriginIsolated) SharedArrayBuffer = ArrayBuffer;
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
@ -14,7 +14,7 @@ import VideoLibraryOutlinedIcon from '@material-ui/icons/VideoLibraryOutlined'
|
|||||||
import config from '../config'
|
import config from '../config'
|
||||||
import DynamicMenuIcon from '../layout/DynamicMenuIcon'
|
import DynamicMenuIcon from '../layout/DynamicMenuIcon'
|
||||||
|
|
||||||
export default {
|
const albumLists = {
|
||||||
all: {
|
all: {
|
||||||
icon: (
|
icon: (
|
||||||
<DynamicMenuIcon
|
<DynamicMenuIcon
|
||||||
@ -79,4 +79,5 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default albumLists
|
||||||
export const defaultAlbumList = 'recentlyAdded'
|
export const defaultAlbumList = 'recentlyAdded'
|
||||||
|
@ -111,7 +111,6 @@ const Player = () => {
|
|||||||
const dataProvider = useDataProvider()
|
const dataProvider = useDataProvider()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const queue = useSelector((state) => state.queue)
|
const queue = useSelector((state) => state.queue)
|
||||||
const current = queue.current || {}
|
|
||||||
const { authenticated } = useAuthState()
|
const { authenticated } = useAuthState()
|
||||||
const showNotifications = useSelector(
|
const showNotifications = useSelector(
|
||||||
(state) => state.settings.notifications || false
|
(state) => state.settings.notifications || false
|
||||||
@ -160,7 +159,8 @@ const Player = () => {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = useMemo(
|
||||||
|
() => ({
|
||||||
theme: playerTheme,
|
theme: playerTheme,
|
||||||
bounds: 'body',
|
bounds: 'body',
|
||||||
mode: 'full',
|
mode: 'full',
|
||||||
@ -211,9 +211,12 @@ const Player = () => {
|
|||||||
shufflePlay: translate('player.playModeText.shufflePlay'),
|
shufflePlay: translate('player.playModeText.shufflePlay'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}),
|
||||||
|
[isDesktop, playerTheme, translate]
|
||||||
|
)
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
|
const current = queue.current || {}
|
||||||
return {
|
return {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
clearPriorAudioLists: queue.clear,
|
clearPriorAudioLists: queue.clear,
|
||||||
@ -223,14 +226,7 @@ const Player = () => {
|
|||||||
extendsContent: <PlayerToolbar id={current.trackId} />,
|
extendsContent: <PlayerToolbar id={current.trackId} />,
|
||||||
defaultVolume: queue.volume,
|
defaultVolume: queue.volume,
|
||||||
}
|
}
|
||||||
}, [
|
}, [queue, defaultOptions])
|
||||||
queue.clear,
|
|
||||||
queue.queue,
|
|
||||||
queue.volume,
|
|
||||||
queue.playIndex,
|
|
||||||
current,
|
|
||||||
defaultOptions,
|
|
||||||
])
|
|
||||||
|
|
||||||
const onAudioListsChange = useCallback(
|
const onAudioListsChange = useCallback(
|
||||||
(currentPlayIndex, audioLists) =>
|
(currentPlayIndex, audioLists) =>
|
||||||
|
@ -7,11 +7,11 @@ import { AddToPlaylistDialog } from './AddToPlaylistDialog'
|
|||||||
describe('AddToPlaylistDialog', () => {
|
describe('AddToPlaylistDialog', () => {
|
||||||
afterEach(cleanup)
|
afterEach(cleanup)
|
||||||
|
|
||||||
let mockData = [
|
const mockData = [
|
||||||
{ id: 'sample-id1', name: 'sample playlist 1', owner: 'admin' },
|
{ id: 'sample-id1', name: 'sample playlist 1', owner: 'admin' },
|
||||||
{ id: 'sample-id2', name: 'sample playlist 2', owner: 'admin' },
|
{ id: 'sample-id2', name: 'sample playlist 2', owner: 'admin' },
|
||||||
]
|
]
|
||||||
let mockIndexedData = {
|
const mockIndexedData = {
|
||||||
'sample-id1': {
|
'sample-id1': {
|
||||||
id: 'sample-id1',
|
id: 'sample-id1',
|
||||||
name: 'sample playlist 1',
|
name: 'sample playlist 1',
|
||||||
@ -23,20 +23,19 @@ describe('AddToPlaylistDialog', () => {
|
|||||||
owner: 'admin',
|
owner: 'admin',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
let selectedIds = ['song-1', 'song-2']
|
const selectedIds = ['song-1', 'song-2']
|
||||||
|
|
||||||
it('adds distinct songs to already existing playlists', async () => {
|
it('adds distinct songs to already existing playlists', async () => {
|
||||||
let mockDataProvider = {
|
const mockDataProvider = {
|
||||||
getList: jest.fn(() =>
|
getList: jest
|
||||||
Promise.resolve({ data: mockData, total: mockData.length })
|
.fn()
|
||||||
),
|
.mockResolvedValue({ data: mockData, total: mockData.length }),
|
||||||
getOne: jest.fn(() =>
|
getOne: jest.fn().mockResolvedValue({ data: { id: 'song-3' }, total: 1 }),
|
||||||
Promise.resolve({ data: { id: 'song-3' }, total: 1 })
|
create: jest.fn().mockResolvedValue({
|
||||||
),
|
data: { id: 'created-id', name: 'created-name' },
|
||||||
create: jest.fn(() =>
|
}),
|
||||||
Promise.resolve({ data: { id: 'created-id', name: 'created-name' } })
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const testutils = render(
|
const testutils = render(
|
||||||
<DataProviderContext.Provider value={mockDataProvider}>
|
<DataProviderContext.Provider value={mockDataProvider}>
|
||||||
<TestContext
|
<TestContext
|
||||||
@ -101,19 +100,16 @@ describe('AddToPlaylistDialog', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
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 () => {
|
it('adds distinct songs to a new playlist', async () => {
|
||||||
|
const mockDataProvider = {
|
||||||
|
getList: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ data: mockData, total: mockData.length }),
|
||||||
|
getOne: jest.fn().mockResolvedValue({ data: { id: 'song-3' }, total: 1 }),
|
||||||
|
create: jest.fn().mockResolvedValue({
|
||||||
|
data: { id: 'created-id1', name: 'created-name' },
|
||||||
|
}),
|
||||||
|
}
|
||||||
const testutils = render(
|
const testutils = render(
|
||||||
<DataProviderContext.Provider value={mockDataProvider}>
|
<DataProviderContext.Provider value={mockDataProvider}>
|
||||||
<TestContext
|
<TestContext
|
||||||
@ -172,6 +168,15 @@ describe('AddToPlaylistDialog', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('adds distinct songs to multiple new playlists', async () => {
|
it('adds distinct songs to multiple new playlists', async () => {
|
||||||
|
const mockDataProvider = {
|
||||||
|
getList: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ data: mockData, total: mockData.length }),
|
||||||
|
getOne: jest.fn().mockResolvedValue({ data: { id: 'song-3' }, total: 1 }),
|
||||||
|
create: jest.fn().mockResolvedValue({
|
||||||
|
data: { id: 'created-id1', name: 'created-name' },
|
||||||
|
}),
|
||||||
|
}
|
||||||
const testutils = render(
|
const testutils = render(
|
||||||
<DataProviderContext.Provider value={mockDataProvider}>
|
<DataProviderContext.Provider value={mockDataProvider}>
|
||||||
<TestContext
|
<TestContext
|
||||||
|
@ -27,9 +27,9 @@ describe('SelectPlaylistInput', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mockDataProvider = {
|
const mockDataProvider = {
|
||||||
getList: jest.fn(() =>
|
getList: jest
|
||||||
Promise.resolve({ data: mockData, total: mockData.length })
|
.fn()
|
||||||
),
|
.mockResolvedValue({ data: mockData, total: mockData.length }),
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// React Hook to get a list of all languages available. English is hardcoded
|
// React Hook to get a list of all languages available. English is hardcoded
|
||||||
import { useGetList } from 'react-admin'
|
import { useGetList } from 'react-admin'
|
||||||
|
|
||||||
export default () => {
|
const useGetLanguageChoices = () => {
|
||||||
const { ids, data, loaded, loading } = useGetList(
|
const { ids, data, loaded, loading } = useGetList(
|
||||||
'translation',
|
'translation',
|
||||||
{ page: 1, perPage: -1 },
|
{ page: 1, perPage: -1 },
|
||||||
@ -17,3 +17,5 @@ export default () => {
|
|||||||
|
|
||||||
return { choices, loaded, loading }
|
return { choices, loaded, loading }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useGetLanguageChoices
|
||||||
|
@ -4,7 +4,12 @@ import './index.css'
|
|||||||
import App from './App'
|
import App from './App'
|
||||||
import * as serviceWorker from './serviceWorker'
|
import * as serviceWorker from './serviceWorker'
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById('root'))
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
)
|
||||||
|
|
||||||
// If you want your app to work offline and load faster, you can change
|
// If you want your app to work offline and load faster, you can change
|
||||||
// unregister() to register() below. Note this comes with some pitfalls.
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { Layout, toggleSidebar } from 'react-admin'
|
import { Layout as RALayout, toggleSidebar } from 'react-admin'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { HotKeys } from 'react-hotkeys'
|
import { HotKeys } from 'react-hotkeys'
|
||||||
import Menu from './Menu'
|
import Menu from './Menu'
|
||||||
@ -12,7 +12,7 @@ const useStyles = makeStyles({
|
|||||||
root: { paddingBottom: (props) => (props.addPadding ? '80px' : 0) },
|
root: { paddingBottom: (props) => (props.addPadding ? '80px' : 0) },
|
||||||
})
|
})
|
||||||
|
|
||||||
export default (props) => {
|
const Layout = (props) => {
|
||||||
const theme = useCurrentTheme()
|
const theme = useCurrentTheme()
|
||||||
const queue = useSelector((state) => state.queue)
|
const queue = useSelector((state) => state.queue)
|
||||||
const classes = useStyles({ addPadding: queue.queue.length > 0 })
|
const classes = useStyles({ addPadding: queue.queue.length > 0 })
|
||||||
@ -24,7 +24,7 @@ export default (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={keyHandlers}>
|
<HotKeys handlers={keyHandlers}>
|
||||||
<Layout
|
<RALayout
|
||||||
{...props}
|
{...props}
|
||||||
className={classes.root}
|
className={classes.root}
|
||||||
menu={Menu}
|
menu={Menu}
|
||||||
@ -35,3 +35,5 @@ export default (props) => {
|
|||||||
</HotKeys>
|
</HotKeys>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Layout
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
import { Logout } from 'react-admin'
|
import { Logout as RALogout } from 'react-admin'
|
||||||
import { clearQueue } from '../actions'
|
import { clearQueue } from '../actions'
|
||||||
|
|
||||||
export default (props) => {
|
const Logout = (props) => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const handleClick = useCallback(() => dispatch(clearQueue()), [dispatch])
|
const handleClick = useCallback(() => dispatch(clearQueue()), [dispatch])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span onClick={handleClick}>
|
<span onClick={handleClick}>
|
||||||
<Logout {...props} />
|
<RALogout {...props} />
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Logout
|
||||||
|
@ -2,4 +2,6 @@ import React from 'react'
|
|||||||
import { Route } from 'react-router-dom'
|
import { Route } from 'react-router-dom'
|
||||||
import Personal from './personal/Personal'
|
import Personal from './personal/Personal'
|
||||||
|
|
||||||
export default [<Route exact path="/personal" render={() => <Personal />} />]
|
const routes = [<Route exact path="/personal" render={() => <Personal />} />]
|
||||||
|
|
||||||
|
export default routes
|
||||||
|
@ -7,7 +7,7 @@ import throttle from 'lodash.throttle'
|
|||||||
import pick from 'lodash.pick'
|
import pick from 'lodash.pick'
|
||||||
import { loadState, saveState } from './persistState'
|
import { loadState, saveState } from './persistState'
|
||||||
|
|
||||||
export default ({
|
const createAdminStore = ({
|
||||||
authProvider,
|
authProvider,
|
||||||
dataProvider,
|
dataProvider,
|
||||||
history,
|
history,
|
||||||
@ -59,3 +59,5 @@ export default ({
|
|||||||
sagaMiddleware.run(saga)
|
sagaMiddleware.run(saga)
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default createAdminStore
|
||||||
|
@ -4,7 +4,7 @@ import themes from './index'
|
|||||||
import { AUTO_THEME_ID } from '../consts'
|
import { AUTO_THEME_ID } from '../consts'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
|
||||||
export default () => {
|
const useCurrentTheme = () => {
|
||||||
const prefersLightMode = useMediaQuery('(prefers-color-scheme: light)')
|
const prefersLightMode = useMediaQuery('(prefers-color-scheme: light)')
|
||||||
return useSelector((state) => {
|
return useSelector((state) => {
|
||||||
if (state.theme === AUTO_THEME_ID) {
|
if (state.theme === AUTO_THEME_ID) {
|
||||||
@ -19,3 +19,5 @@ export default () => {
|
|||||||
return themes[themeName]
|
return themes[themeName]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useCurrentTheme
|
||||||
|
Loading…
x
Reference in New Issue
Block a user