mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-07 06:27:36 +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
15 changed files with 6337 additions and 6515 deletions
4
.github/workflows/pipeline.yml
vendored
4
.github/workflows/pipeline.yml
vendored
|
@ -85,10 +85,10 @@ jobs:
|
|||
cd ui
|
||||
npm ci
|
||||
|
||||
- name: npm check-formatting
|
||||
- name: npm lint
|
||||
run: |
|
||||
cd ui
|
||||
npm run check-formatting
|
||||
npm run check-formatting && npm run lint
|
||||
|
||||
- name: npm test
|
||||
run: |
|
||||
|
|
12584
ui/package-lock.json
generated
12584
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.throttle": "^4.1.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"ra-data-json-server": "^3.15.2",
|
||||
"ra-i18n-polyglot": "^3.15.2",
|
||||
"react": "^16.14.0",
|
||||
"react-admin": "^3.15.2",
|
||||
"react-dom": "^16.14.0",
|
||||
"ra-data-json-server": "^3.15.1",
|
||||
"ra-i18n-polyglot": "^3.15.1",
|
||||
"react": "^17.0.2",
|
||||
"react-admin": "^3.15.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-drag-listview": "^0.1.8",
|
||||
"react-ga": "^3.3.0",
|
||||
"react-hotkeys": "^2.0.0",
|
||||
"react-icons": "^4.2.0",
|
||||
"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-redux": "^7.2.4",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "^3.4.3",
|
||||
"react-scripts": "^4.0.3",
|
||||
"redux": "^4.1.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
"uuid": "^8.3.2"
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.12.0",
|
||||
|
@ -42,15 +43,14 @@
|
|||
"@testing-library/react-hooks": "^5.1.2",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"css-mediaquery": "^0.1.2",
|
||||
"jest-environment-jsdom-sixteen": "^2.0.0",
|
||||
"prettier": "2.3.0",
|
||||
"ra-test": "^3.15.2"
|
||||
"ra-test": "^3.15.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jest-environment-jsdom-sixteen",
|
||||
"lint": "eslint -c node_modules/eslint-config-react-app/index.js src/**/*.js",
|
||||
"test": "react-scripts test",
|
||||
"lint": "eslint --max-warnings 0 src/**/*.js",
|
||||
"eject": "react-scripts eject",
|
||||
"prettier": "prettier --write src/*.js src/**/*.js",
|
||||
"check-formatting": "prettier -c src/*.js src/**/*.js"
|
||||
|
@ -58,7 +58,21 @@
|
|||
"homepage": ".",
|
||||
"proxy": "http://localhost:4633/",
|
||||
"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": {
|
||||
"production": [
|
||||
|
|
|
@ -33,11 +33,6 @@
|
|||
<script>
|
||||
window.__APP_CONFIG__ = "{{.AppConfig}}"
|
||||
</script>
|
||||
<!-- Issue workaround for React v16. -->
|
||||
<script>
|
||||
// See https://github.com/facebook/react/issues/20829#issuecomment-802088260
|
||||
if (!crossOriginIsolated) SharedArrayBuffer = ArrayBuffer;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<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 DynamicMenuIcon from '../layout/DynamicMenuIcon'
|
||||
|
||||
export default {
|
||||
const albumLists = {
|
||||
all: {
|
||||
icon: (
|
||||
<DynamicMenuIcon
|
||||
|
@ -79,4 +79,5 @@ export default {
|
|||
},
|
||||
}
|
||||
|
||||
export default albumLists
|
||||
export const defaultAlbumList = 'recentlyAdded'
|
||||
|
|
|
@ -111,7 +111,6 @@ const Player = () => {
|
|||
const dataProvider = useDataProvider()
|
||||
const dispatch = useDispatch()
|
||||
const queue = useSelector((state) => state.queue)
|
||||
const current = queue.current || {}
|
||||
const { authenticated } = useAuthState()
|
||||
const showNotifications = useSelector(
|
||||
(state) => state.settings.notifications || false
|
||||
|
@ -160,60 +159,64 @@ const Player = () => {
|
|||
),
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
theme: playerTheme,
|
||||
bounds: 'body',
|
||||
mode: 'full',
|
||||
autoPlay: false,
|
||||
preload: true,
|
||||
autoPlayInitLoadPlayList: true,
|
||||
loadAudioErrorPlayNext: false,
|
||||
clearPriorAudioLists: false,
|
||||
showDestroy: true,
|
||||
showDownload: false,
|
||||
showReload: false,
|
||||
toggleMode: !isDesktop,
|
||||
glassBg: false,
|
||||
showThemeSwitch: false,
|
||||
showMediaSession: true,
|
||||
restartCurrentOnPrev: true,
|
||||
defaultPosition: {
|
||||
top: 300,
|
||||
left: 120,
|
||||
},
|
||||
volumeFade: { fadeIn: 200, fadeOut: 200 },
|
||||
renderAudioTitle: (audioInfo, isMobile) => (
|
||||
<AudioTitle audioInfo={audioInfo} isMobile={isMobile} />
|
||||
),
|
||||
locale: {
|
||||
playListsText: translate('player.playListsText'),
|
||||
openText: translate('player.openText'),
|
||||
closeText: translate('player.closeText'),
|
||||
notContentText: translate('player.notContentText'),
|
||||
clickToPlayText: translate('player.clickToPlayText'),
|
||||
clickToPauseText: translate('player.clickToPauseText'),
|
||||
nextTrackText: translate('player.nextTrackText'),
|
||||
previousTrackText: translate('player.previousTrackText'),
|
||||
reloadText: translate('player.reloadText'),
|
||||
volumeText: translate('player.volumeText'),
|
||||
toggleLyricText: translate('player.toggleLyricText'),
|
||||
toggleMiniModeText: translate('player.toggleMiniModeText'),
|
||||
destroyText: translate('player.destroyText'),
|
||||
downloadText: translate('player.downloadText'),
|
||||
removeAudioListsText: translate('player.removeAudioListsText'),
|
||||
clickToDeleteText: (name) =>
|
||||
translate('player.clickToDeleteText', { name }),
|
||||
emptyLyricText: translate('player.emptyLyricText'),
|
||||
playModeText: {
|
||||
order: translate('player.playModeText.order'),
|
||||
orderLoop: translate('player.playModeText.orderLoop'),
|
||||
singleLoop: translate('player.playModeText.singleLoop'),
|
||||
shufflePlay: translate('player.playModeText.shufflePlay'),
|
||||
const defaultOptions = useMemo(
|
||||
() => ({
|
||||
theme: playerTheme,
|
||||
bounds: 'body',
|
||||
mode: 'full',
|
||||
autoPlay: false,
|
||||
preload: true,
|
||||
autoPlayInitLoadPlayList: true,
|
||||
loadAudioErrorPlayNext: false,
|
||||
clearPriorAudioLists: false,
|
||||
showDestroy: true,
|
||||
showDownload: false,
|
||||
showReload: false,
|
||||
toggleMode: !isDesktop,
|
||||
glassBg: false,
|
||||
showThemeSwitch: false,
|
||||
showMediaSession: true,
|
||||
restartCurrentOnPrev: true,
|
||||
defaultPosition: {
|
||||
top: 300,
|
||||
left: 120,
|
||||
},
|
||||
},
|
||||
}
|
||||
volumeFade: { fadeIn: 200, fadeOut: 200 },
|
||||
renderAudioTitle: (audioInfo, isMobile) => (
|
||||
<AudioTitle audioInfo={audioInfo} isMobile={isMobile} />
|
||||
),
|
||||
locale: {
|
||||
playListsText: translate('player.playListsText'),
|
||||
openText: translate('player.openText'),
|
||||
closeText: translate('player.closeText'),
|
||||
notContentText: translate('player.notContentText'),
|
||||
clickToPlayText: translate('player.clickToPlayText'),
|
||||
clickToPauseText: translate('player.clickToPauseText'),
|
||||
nextTrackText: translate('player.nextTrackText'),
|
||||
previousTrackText: translate('player.previousTrackText'),
|
||||
reloadText: translate('player.reloadText'),
|
||||
volumeText: translate('player.volumeText'),
|
||||
toggleLyricText: translate('player.toggleLyricText'),
|
||||
toggleMiniModeText: translate('player.toggleMiniModeText'),
|
||||
destroyText: translate('player.destroyText'),
|
||||
downloadText: translate('player.downloadText'),
|
||||
removeAudioListsText: translate('player.removeAudioListsText'),
|
||||
clickToDeleteText: (name) =>
|
||||
translate('player.clickToDeleteText', { name }),
|
||||
emptyLyricText: translate('player.emptyLyricText'),
|
||||
playModeText: {
|
||||
order: translate('player.playModeText.order'),
|
||||
orderLoop: translate('player.playModeText.orderLoop'),
|
||||
singleLoop: translate('player.playModeText.singleLoop'),
|
||||
shufflePlay: translate('player.playModeText.shufflePlay'),
|
||||
},
|
||||
},
|
||||
}),
|
||||
[isDesktop, playerTheme, translate]
|
||||
)
|
||||
|
||||
const options = useMemo(() => {
|
||||
const current = queue.current || {}
|
||||
return {
|
||||
...defaultOptions,
|
||||
clearPriorAudioLists: queue.clear,
|
||||
|
@ -223,14 +226,7 @@ const Player = () => {
|
|||
extendsContent: <PlayerToolbar id={current.trackId} />,
|
||||
defaultVolume: queue.volume,
|
||||
}
|
||||
}, [
|
||||
queue.clear,
|
||||
queue.queue,
|
||||
queue.volume,
|
||||
queue.playIndex,
|
||||
current,
|
||||
defaultOptions,
|
||||
])
|
||||
}, [queue, defaultOptions])
|
||||
|
||||
const onAudioListsChange = useCallback(
|
||||
(currentPlayIndex, audioLists) =>
|
||||
|
|
|
@ -7,11 +7,11 @@ import { AddToPlaylistDialog } from './AddToPlaylistDialog'
|
|||
describe('AddToPlaylistDialog', () => {
|
||||
afterEach(cleanup)
|
||||
|
||||
let mockData = [
|
||||
const mockData = [
|
||||
{ id: 'sample-id1', name: 'sample playlist 1', owner: 'admin' },
|
||||
{ id: 'sample-id2', name: 'sample playlist 2', owner: 'admin' },
|
||||
]
|
||||
let mockIndexedData = {
|
||||
const mockIndexedData = {
|
||||
'sample-id1': {
|
||||
id: 'sample-id1',
|
||||
name: 'sample playlist 1',
|
||||
|
@ -23,20 +23,19 @@ describe('AddToPlaylistDialog', () => {
|
|||
owner: 'admin',
|
||||
},
|
||||
}
|
||||
let selectedIds = ['song-1', 'song-2']
|
||||
const 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 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-id', name: 'created-name' },
|
||||
}),
|
||||
}
|
||||
|
||||
const testutils = render(
|
||||
<DataProviderContext.Provider value={mockDataProvider}>
|
||||
<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 () => {
|
||||
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(
|
||||
<DataProviderContext.Provider value={mockDataProvider}>
|
||||
<TestContext
|
||||
|
@ -172,6 +168,15 @@ describe('AddToPlaylistDialog', () => {
|
|||
})
|
||||
|
||||
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(
|
||||
<DataProviderContext.Provider value={mockDataProvider}>
|
||||
<TestContext
|
||||
|
|
|
@ -27,9 +27,9 @@ describe('SelectPlaylistInput', () => {
|
|||
}
|
||||
|
||||
const mockDataProvider = {
|
||||
getList: jest.fn(() =>
|
||||
Promise.resolve({ data: mockData, total: mockData.length })
|
||||
),
|
||||
getList: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ data: mockData, total: mockData.length }),
|
||||
}
|
||||
|
||||
render(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// React Hook to get a list of all languages available. English is hardcoded
|
||||
import { useGetList } from 'react-admin'
|
||||
|
||||
export default () => {
|
||||
const useGetLanguageChoices = () => {
|
||||
const { ids, data, loaded, loading } = useGetList(
|
||||
'translation',
|
||||
{ page: 1, perPage: -1 },
|
||||
|
@ -17,3 +17,5 @@ export default () => {
|
|||
|
||||
return { choices, loaded, loading }
|
||||
}
|
||||
|
||||
export default useGetLanguageChoices
|
||||
|
|
|
@ -4,7 +4,12 @@ import './index.css'
|
|||
import App from './App'
|
||||
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
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback } from 'react'
|
||||
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 { HotKeys } from 'react-hotkeys'
|
||||
import Menu from './Menu'
|
||||
|
@ -12,7 +12,7 @@ const useStyles = makeStyles({
|
|||
root: { paddingBottom: (props) => (props.addPadding ? '80px' : 0) },
|
||||
})
|
||||
|
||||
export default (props) => {
|
||||
const Layout = (props) => {
|
||||
const theme = useCurrentTheme()
|
||||
const queue = useSelector((state) => state.queue)
|
||||
const classes = useStyles({ addPadding: queue.queue.length > 0 })
|
||||
|
@ -24,7 +24,7 @@ export default (props) => {
|
|||
|
||||
return (
|
||||
<HotKeys handlers={keyHandlers}>
|
||||
<Layout
|
||||
<RALayout
|
||||
{...props}
|
||||
className={classes.root}
|
||||
menu={Menu}
|
||||
|
@ -35,3 +35,5 @@ export default (props) => {
|
|||
</HotKeys>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import React, { useCallback } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { Logout } from 'react-admin'
|
||||
import { Logout as RALogout } from 'react-admin'
|
||||
import { clearQueue } from '../actions'
|
||||
|
||||
export default (props) => {
|
||||
const Logout = (props) => {
|
||||
const dispatch = useDispatch()
|
||||
const handleClick = useCallback(() => dispatch(clearQueue()), [dispatch])
|
||||
|
||||
return (
|
||||
<span onClick={handleClick}>
|
||||
<Logout {...props} />
|
||||
<RALogout {...props} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default Logout
|
||||
|
|
|
@ -2,4 +2,6 @@ import React from 'react'
|
|||
import { Route } from 'react-router-dom'
|
||||
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 { loadState, saveState } from './persistState'
|
||||
|
||||
export default ({
|
||||
const createAdminStore = ({
|
||||
authProvider,
|
||||
dataProvider,
|
||||
history,
|
||||
|
@ -59,3 +59,5 @@ export default ({
|
|||
sagaMiddleware.run(saga)
|
||||
return store
|
||||
}
|
||||
|
||||
export default createAdminStore
|
||||
|
|
|
@ -4,7 +4,7 @@ import themes from './index'
|
|||
import { AUTO_THEME_ID } from '../consts'
|
||||
import config from '../config'
|
||||
|
||||
export default () => {
|
||||
const useCurrentTheme = () => {
|
||||
const prefersLightMode = useMediaQuery('(prefers-color-scheme: light)')
|
||||
return useSelector((state) => {
|
||||
if (state.theme === AUTO_THEME_ID) {
|
||||
|
@ -19,3 +19,5 @@ export default () => {
|
|||
return themes[themeName]
|
||||
})
|
||||
}
|
||||
|
||||
export default useCurrentTheme
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue