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:
Deluan Quintão 2021-05-25 09:58:06 -04:00 committed by GitHub
parent d9f268266c
commit 5631493cc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 6337 additions and 6515 deletions

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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": [

View File

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

View File

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

View File

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

View File

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

View File

@ -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(

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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