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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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