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:
@@ -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,
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) =>
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user