mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 10:27:08 +02:00
Compare commits
2 commits
master
...
t3code/per
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feb3355407 | ||
|
|
4112b73ac7 |
25 changed files with 113 additions and 142 deletions
|
|
@ -374,11 +374,8 @@ func (r *resolver) resolvePlaylist(ctx context.Context, playlistID string) (reso
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks := r.ds.Playlist().Tracks(ctx, pl.ID, false)
|
albumIDs, err := r.ds.Playlist().Tracks(ctx, pl.ID, false).
|
||||||
if tracks == nil {
|
GetAlbumIDs(ctx, model.QueryOptions{Max: PlaylistGridSamples, Sort: "random()"})
|
||||||
return resolution{}, fmt.Errorf("resolvePlaylist: could not load tracks for playlist %s", pl.ID)
|
|
||||||
}
|
|
||||||
albumIDs, err := tracks.GetAlbumIDs(ctx, model.QueryOptions{Max: PlaylistGridSamples, Sort: "random()"})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resolution{}, err
|
return resolution{}, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -707,16 +707,6 @@ var _ = Describe("resolveItem", func() {
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
Expect(res).To(Equal(resolution{}))
|
Expect(res).To(Equal(resolution{}))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("returns an error when the playlist tracks cannot be loaded", func() {
|
|
||||||
plRepo := tests.CreateMockPlaylistRepo()
|
|
||||||
plRepo.SetData(model.Playlists{{ID: "pl4", Name: "Playlist"}})
|
|
||||||
ds.MockedPlaylist = plRepo
|
|
||||||
|
|
||||||
res, err := newResolver(ds, ag, ffm, nil).resolve(ctx, model.ArtworkQueueItem{ItemKind: "pl", ItemID: "pl4"})
|
|
||||||
Expect(err).To(HaveOccurred())
|
|
||||||
Expect(res).To(Equal(resolution{}))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,9 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"runtime/debug"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -246,7 +244,7 @@ func (w *Worker) process(ctx context.Context, item model.ArtworkQueueItem) (outc
|
||||||
item.ImageType = cmp.Or(item.ImageType, model.ImageTypePrimary)
|
item.ImageType = cmp.Or(item.ImageType, model.ImageTypePrimary)
|
||||||
trace := &ChainTrace{}
|
trace := &ChainTrace{}
|
||||||
ctx = withTrace(ctx, trace)
|
ctx = withTrace(ctx, trace)
|
||||||
out, got, retryIn := w.safeAcquire(ctx, item)
|
out, got, retryIn := w.proc.acquire(ctx, item)
|
||||||
|
|
||||||
queue := w.proc.ds.ArtworkQueue()
|
queue := w.proc.ds.ArtworkQueue()
|
||||||
switch out {
|
switch out {
|
||||||
|
|
@ -288,20 +286,6 @@ func (w *Worker) process(ctx context.Context, item model.ArtworkQueueItem) (outc
|
||||||
return out, got
|
return out, got
|
||||||
}
|
}
|
||||||
|
|
||||||
// safeAcquire turns a panic into a failed attempt: the drain runs on a bare goroutine, so an
|
|
||||||
// unrecovered panic would crash the server, and the still-queued row would crash it again on restart.
|
|
||||||
func (w *Worker) safeAcquire(ctx context.Context, item model.ArtworkQueueItem) (out outcome, got *acquired, retryIn time.Duration) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
log.Error(ctx, "Artwork: Panic while processing item", "kind", item.ItemKind, "id", item.ItemID,
|
|
||||||
"imageType", item.ImageType, "attempts", item.Attempts, "panic", r, "stack", string(debug.Stack()))
|
|
||||||
traceStage(ctx, "panic", fmt.Errorf("%v", r))
|
|
||||||
out, got, retryIn = outcomeFailed, nil, 0
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return w.proc.acquire(ctx, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// recordGiveUp keeps the last failure on the state row after the queue row is deleted. An item
|
// recordGiveUp keeps the last failure on the state row after the queue row is deleted. An item
|
||||||
// that never resolved has no row to update, and creating one would settle it absent.
|
// that never resolved has no row to update, and creating one would settle it absent.
|
||||||
func (w *Worker) recordGiveUp(ctx context.Context, item model.ArtworkQueueItem, trace string) {
|
func (w *Worker) recordGiveUp(ctx context.Context, item model.ArtworkQueueItem, trace string) {
|
||||||
|
|
|
||||||
|
|
@ -142,18 +142,6 @@ func (v *visibilityPlaylistRepo) Get(ctx context.Context, id string) (*model.Pla
|
||||||
return v.MockPlaylistRepo.Get(ctx, id)
|
return v.MockPlaylistRepo.Get(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
type panickingAlbumRepo struct {
|
|
||||||
*tests.MockAlbumRepo
|
|
||||||
panicID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *panickingAlbumRepo) Get(ctx context.Context, id string) (*model.Album, error) {
|
|
||||||
if id == r.panicID {
|
|
||||||
panic("boom")
|
|
||||||
}
|
|
||||||
return r.MockAlbumRepo.Get(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func adminUserRepo() *tests.MockedUserRepo {
|
func adminUserRepo() *tests.MockedUserRepo {
|
||||||
repo := tests.CreateMockUserRepo()
|
repo := tests.CreateMockUserRepo()
|
||||||
Expect(repo.Put(GinkgoT().Context(), &model.User{ID: "admin", UserName: "admin", IsAdmin: true})).To(Succeed())
|
Expect(repo.Put(GinkgoT().Context(), &model.User{ID: "admin", UserName: "admin", IsAdmin: true})).To(Succeed())
|
||||||
|
|
@ -290,36 +278,6 @@ var _ = Describe("Worker", func() {
|
||||||
Expect(err).To(MatchError(model.ErrNotFound), "a timeout must never settle on absent")
|
Expect(err).To(MatchError(model.ErrNotFound), "a timeout must never settle on absent")
|
||||||
})
|
})
|
||||||
|
|
||||||
It("fails an item that panics, without stopping the rest of the batch", func() {
|
|
||||||
folderRepo.result = []model.Folder{{
|
|
||||||
Path: "tests/fixtures/artist/an-album",
|
|
||||||
ImageFiles: []string{"cover.jpg"},
|
|
||||||
}}
|
|
||||||
albums := tests.CreateMockAlbumRepo()
|
|
||||||
albums.SetData(model.Albums{
|
|
||||||
{ID: "alboom", Name: "Album", FolderIDs: []string{"f1"}},
|
|
||||||
{ID: "alok", Name: "Album", FolderIDs: []string{"f1"}},
|
|
||||||
})
|
|
||||||
ds.MockedAlbum = &panickingAlbumRepo{MockAlbumRepo: albums, panicID: "alboom"}
|
|
||||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alboom"})).To(Succeed())
|
|
||||||
Expect(queueRepo.Enqueue(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alok"})).To(Succeed())
|
|
||||||
|
|
||||||
n, err := w.drain(ctx, 1)
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
|
||||||
Expect(n).To(Equal(2))
|
|
||||||
|
|
||||||
it := findQueued(queueRepo, "al", "alboom")
|
|
||||||
Expect(it).ToNot(BeNil(), "a panicking item must be rescheduled, not dropped")
|
|
||||||
Expect(it.Attempts).To(Equal(1))
|
|
||||||
Expect(it.RetryAt).To(BeTemporally(">", time.Now()))
|
|
||||||
Expect(it.Trace).To(ContainSubstring("boom"))
|
|
||||||
|
|
||||||
Expect(findQueued(queueRepo, "al", "alok")).To(BeNil())
|
|
||||||
ia, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "alok", model.ImageTypePrimary)
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
|
||||||
Expect(ia.Source).To(Equal("folder"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("reschedules past the provider's requested delay when it exceeds the backoff", func() {
|
It("reschedules past the provider's requested delay when it exceeds the backoff", func() {
|
||||||
conf.Server.CoverArtPriority = "external"
|
conf.Server.CoverArtPriority = "external"
|
||||||
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "al9", Name: "Album"}})
|
ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "al9", Name: "Album"}})
|
||||||
|
|
|
||||||
|
|
@ -660,6 +660,11 @@
|
||||||
"options": {
|
"options": {
|
||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"language": "Língua",
|
"language": "Língua",
|
||||||
|
"dateFormat": "Formato de data",
|
||||||
|
"dateFormats": {
|
||||||
|
"language": "Igual à língua",
|
||||||
|
"browser": "Igual ao navegador"
|
||||||
|
},
|
||||||
"defaultView": "Tela inicial",
|
"defaultView": "Tela inicial",
|
||||||
"desktop_notifications": "Notificações",
|
"desktop_notifications": "Notificações",
|
||||||
"lastfmScrobbling": "Enviar scrobbles para Last.fm",
|
"lastfmScrobbling": "Enviar scrobbles para Last.fm",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
ArrayField,
|
ArrayField,
|
||||||
BooleanField,
|
BooleanField,
|
||||||
ChipField,
|
ChipField,
|
||||||
DateField,
|
|
||||||
FunctionField,
|
FunctionField,
|
||||||
SingleFieldList,
|
SingleFieldList,
|
||||||
TextField,
|
TextField,
|
||||||
|
|
@ -17,6 +16,7 @@ import {
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import {
|
import {
|
||||||
|
DateField,
|
||||||
ArtistLinkField,
|
ArtistLinkField,
|
||||||
MultiLineTextField,
|
MultiLineTextField,
|
||||||
ParticipantsInfo,
|
ParticipantsInfo,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DatagridBody,
|
DatagridBody,
|
||||||
DatagridRow,
|
DatagridRow,
|
||||||
DateField,
|
|
||||||
NumberField,
|
NumberField,
|
||||||
TextField,
|
TextField,
|
||||||
FunctionField,
|
FunctionField,
|
||||||
|
|
@ -13,6 +12,7 @@ import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { useDrag } from 'react-dnd'
|
import { useDrag } from 'react-dnd'
|
||||||
import {
|
import {
|
||||||
|
DateField,
|
||||||
ArtistLinkField,
|
ArtistLinkField,
|
||||||
ArtworkAvatar,
|
ArtworkAvatar,
|
||||||
DurationField,
|
DurationField,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { isDateSet } from '../utils/validations'
|
import { isDateSet } from '../utils/validations'
|
||||||
import { DateField as RADateField } from 'react-admin'
|
import { DateField as RADateField, useRecordContext } from 'react-admin'
|
||||||
import { useDateLocale } from '../i18n/useDateLocale'
|
import { useDateLocale } from '../i18n/useDateLocale'
|
||||||
|
|
||||||
export const DateField = (props) => {
|
export const DateField = (props) => {
|
||||||
const { record, source } = props
|
const record = useRecordContext(props)
|
||||||
const locale = useDateLocale()
|
const locale = useDateLocale()
|
||||||
const value = record?.[source]
|
const value = record?.[props.source]
|
||||||
if (!isDateSet(value)) return null
|
if (!isDateSet(value)) return null
|
||||||
return <RADateField locales={locale} {...props} />
|
return <RADateField locales={locale} {...props} />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||||
|
import { RecordContextProvider } from 'react-admin'
|
||||||
import { DateField } from './DateField'
|
import { DateField } from './DateField'
|
||||||
|
|
||||||
vi.mock('react-admin', async (importOriginal) => ({
|
vi.mock('react-admin', async (importOriginal) => ({
|
||||||
|
|
@ -29,4 +30,13 @@ describe('<DateField>', () => {
|
||||||
)
|
)
|
||||||
expect(container).toBeEmptyDOMElement()
|
expect(container).toBeEmptyDOMElement()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('reads the record from the record context', () => {
|
||||||
|
render(
|
||||||
|
<RecordContextProvider value={record}>
|
||||||
|
<DateField source="updatedAt" />
|
||||||
|
</RecordContextProvider>,
|
||||||
|
)
|
||||||
|
expect(screen.getByText('17.9.2026')).toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import TableContainer from '@material-ui/core/TableContainer'
|
||||||
import TableRow from '@material-ui/core/TableRow'
|
import TableRow from '@material-ui/core/TableRow'
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateField,
|
|
||||||
TextField,
|
TextField,
|
||||||
NumberField,
|
NumberField,
|
||||||
FunctionField,
|
FunctionField,
|
||||||
|
|
@ -15,6 +14,7 @@ import {
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import { humanize, underscore } from 'inflection'
|
import { humanize, underscore } from 'inflection'
|
||||||
import {
|
import {
|
||||||
|
DateField,
|
||||||
ArtistLinkField,
|
ArtistLinkField,
|
||||||
BitrateField,
|
BitrateField,
|
||||||
ParticipantsInfo,
|
ParticipantsInfo,
|
||||||
|
|
|
||||||
|
|
@ -677,6 +677,11 @@
|
||||||
"options": {
|
"options": {
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
"dateFormat": "Date Format",
|
||||||
|
"dateFormats": {
|
||||||
|
"language": "Match language",
|
||||||
|
"browser": "Match browser"
|
||||||
|
},
|
||||||
"defaultView": "Default View",
|
"defaultView": "Default View",
|
||||||
"desktop_notifications": "Desktop Notifications",
|
"desktop_notifications": "Desktop Notifications",
|
||||||
"lastfmNotConfigured": "Last.fm API-Key is not configured",
|
"lastfmNotConfigured": "Last.fm API-Key is not configured",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import { useLocale } from 'react-admin'
|
import { useLocale } from 'react-admin'
|
||||||
|
|
||||||
|
export const getStoredDateFormat = () =>
|
||||||
|
localStorage.getItem('dateFormat') || 'language'
|
||||||
|
|
||||||
// Our language codes are mostly region-less ("en"), and Intl reads a bare "en"
|
// Our language codes are mostly region-less ("en"), and Intl reads a bare "en"
|
||||||
// as en-US. Borrow the region from the browser when it speaks the same language.
|
// as en-US. Borrow the region from the browser when it speaks the same language.
|
||||||
const resolveDateLocale = (locale, browserLocales = []) => {
|
const resolveDateLocale = (locale, browserLocales = []) => {
|
||||||
|
|
@ -10,5 +13,10 @@ const resolveDateLocale = (locale, browserLocales = []) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDateLocale = () =>
|
// With the "browser" date format, return undefined so Intl uses the browser's
|
||||||
resolveDateLocale(useLocale(), navigator.languages)
|
// own locale, regardless of the selected language.
|
||||||
|
export const useDateLocale = () => {
|
||||||
|
const locale = useLocale()
|
||||||
|
if (getStoredDateFormat() === 'browser') return undefined
|
||||||
|
return resolveDateLocale(locale, navigator.languages)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ vi.mock('react-admin', () => ({
|
||||||
describe('useDateLocale', () => {
|
describe('useDateLocale', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
localStorage.removeItem('dateFormat')
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderWith = async (locale, browserLocales) => {
|
const renderWith = async (locale, browserLocales) => {
|
||||||
|
|
@ -42,4 +43,10 @@ describe('useDateLocale', () => {
|
||||||
const result = await renderWith(undefined, ['en-GB'])
|
const result = await renderWith(undefined, ['en-GB'])
|
||||||
expect(result.current).toBeUndefined()
|
expect(result.current).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('returns undefined to use the browser locale when set to browser', async () => {
|
||||||
|
localStorage.setItem('dateFormat', 'browser')
|
||||||
|
const result = await renderWith('en', ['fr-FR'])
|
||||||
|
expect(result.current).toBeUndefined()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
DateField,
|
||||||
List,
|
List,
|
||||||
Pagination,
|
Pagination,
|
||||||
SizeField,
|
SizeField,
|
||||||
|
|
@ -7,7 +8,6 @@ import {
|
||||||
} from '../common/index'
|
} from '../common/index'
|
||||||
import {
|
import {
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
|
||||||
TextField,
|
TextField,
|
||||||
downloadCSV,
|
downloadCSV,
|
||||||
Filter,
|
Filter,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { SimpleForm, Title, useTranslate } from 'react-admin'
|
||||||
import { Card } from '@material-ui/core'
|
import { Card } from '@material-ui/core'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { SelectLanguage } from './SelectLanguage'
|
import { SelectLanguage } from './SelectLanguage'
|
||||||
|
import { SelectDateFormat } from './SelectDateFormat'
|
||||||
import { SelectTheme } from './SelectTheme'
|
import { SelectTheme } from './SelectTheme'
|
||||||
import { SelectDefaultView } from './SelectDefaultView'
|
import { SelectDefaultView } from './SelectDefaultView'
|
||||||
import { NotificationsToggle } from './NotificationsToggle'
|
import { NotificationsToggle } from './NotificationsToggle'
|
||||||
|
|
@ -24,6 +25,7 @@ const Personal = () => {
|
||||||
<SimpleForm toolbar={null} variant={'outlined'}>
|
<SimpleForm toolbar={null} variant={'outlined'}>
|
||||||
<SelectTheme />
|
<SelectTheme />
|
||||||
<SelectLanguage />
|
<SelectLanguage />
|
||||||
|
<SelectDateFormat />
|
||||||
<SelectDefaultView />
|
<SelectDefaultView />
|
||||||
{config.enableReplayGain && <ReplayGainToggle />}
|
{config.enableReplayGain && <ReplayGainToggle />}
|
||||||
<NotificationsToggle />
|
<NotificationsToggle />
|
||||||
|
|
|
||||||
22
ui/src/personal/SelectDateFormat.jsx
Normal file
22
ui/src/personal/SelectDateFormat.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { SelectInput, useTranslate } from 'react-admin'
|
||||||
|
import { getStoredDateFormat } from '../i18n/useDateLocale'
|
||||||
|
|
||||||
|
export const SelectDateFormat = (props) => {
|
||||||
|
const translate = useTranslate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectInput
|
||||||
|
{...props}
|
||||||
|
source="dateFormat"
|
||||||
|
label={translate('menu.personal.options.dateFormat')}
|
||||||
|
defaultValue={getStoredDateFormat()}
|
||||||
|
choices={[
|
||||||
|
{ id: 'language', name: 'menu.personal.options.dateFormats.language' },
|
||||||
|
{ id: 'browser', name: 'menu.personal.options.dateFormats.browser' },
|
||||||
|
]}
|
||||||
|
onChange={(event) => {
|
||||||
|
localStorage.setItem('dateFormat', event.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
|
||||||
EditButton,
|
EditButton,
|
||||||
Filter,
|
Filter,
|
||||||
NullableBooleanInput,
|
NullableBooleanInput,
|
||||||
|
|
@ -20,6 +19,7 @@ import Switch from '@material-ui/core/Switch'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { useMediaQuery } from '@material-ui/core'
|
import { useMediaQuery } from '@material-ui/core'
|
||||||
import {
|
import {
|
||||||
|
DateField,
|
||||||
ArtworkAvatar,
|
ArtworkAvatar,
|
||||||
DurationField,
|
DurationField,
|
||||||
List,
|
List,
|
||||||
|
|
@ -95,7 +95,6 @@ export const ToggleField = ({ resource, source }) => {
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
checked={record[source]}
|
checked={record[source]}
|
||||||
color="primary"
|
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
disabled={!isWritable(record.ownerId)}
|
disabled={!isWritable(record.ownerId)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import React from 'react'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { describe, it, expect, vi } from 'vitest'
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
import { TestContext } from 'ra-test'
|
import { TestContext } from 'ra-test'
|
||||||
import { RecordContextProvider } from 'react-admin'
|
|
||||||
import { PlaylistLove, ToggleField, ToggleAutoImport } from './PlaylistList'
|
import { PlaylistLove, ToggleField, ToggleAutoImport } from './PlaylistList'
|
||||||
|
|
||||||
vi.mock('../config', () => ({
|
vi.mock('../config', () => ({
|
||||||
|
|
@ -15,7 +14,6 @@ vi.mock('../common', () => ({
|
||||||
{record?.starred ? 'starred' : 'not-starred'}
|
{record?.starred ? 'starred' : 'not-starred'}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
isWritable: (ownerId) => ownerId === 'me',
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
describe('<PlaylistLove />', () => {
|
describe('<PlaylistLove />', () => {
|
||||||
|
|
@ -57,28 +55,3 @@ describe('playlist toggles without a record', () => {
|
||||||
expect(container.innerHTML).toBe('')
|
expect(container.innerHTML).toBe('')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Secondary is a surface color in many themes, so these toggles must use primary
|
|
||||||
describe('<ToggleField />', () => {
|
|
||||||
const renderToggle = (record) =>
|
|
||||||
render(
|
|
||||||
<TestContext>
|
|
||||||
<RecordContextProvider value={record}>
|
|
||||||
<ToggleField resource="playlist" source="public" />
|
|
||||||
</RecordContextProvider>
|
|
||||||
</TestContext>,
|
|
||||||
)
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
['owner', 'me', false],
|
|
||||||
['non-owner', 'someone-else', true],
|
|
||||||
])('renders a primary-colored switch for the %s', (_, ownerId, disabled) => {
|
|
||||||
renderToggle({ id: 'pl-1', public: true, ownerId })
|
|
||||||
const input = screen.getByRole('checkbox')
|
|
||||||
const switchBase = input.closest('.MuiSwitch-switchBase')
|
|
||||||
expect(input.checked).toBe(true)
|
|
||||||
expect(input.disabled).toBe(disabled)
|
|
||||||
expect(switchBase.classList).toContain('MuiSwitch-colorPrimary')
|
|
||||||
expect(switchBase.classList).not.toContain('MuiSwitch-colorSecondary')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import React, { cloneElement } from 'react'
|
||||||
import {
|
import {
|
||||||
CreateButton,
|
CreateButton,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
|
||||||
EditButton,
|
EditButton,
|
||||||
Filter,
|
Filter,
|
||||||
sanitizeListRestProps,
|
sanitizeListRestProps,
|
||||||
|
|
@ -15,6 +14,7 @@ import {
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import {
|
import {
|
||||||
|
DateField,
|
||||||
List,
|
List,
|
||||||
defaultRowsPerPageOptions,
|
defaultRowsPerPageOptions,
|
||||||
getStoredPerPage,
|
getStoredPerPage,
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,16 @@ export default {
|
||||||
color: `${foreground} !important`,
|
color: `${foreground} !important`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiSwitch: {
|
||||||
|
colorSecondary: {
|
||||||
|
'&$checked': {
|
||||||
|
color: green,
|
||||||
|
},
|
||||||
|
'&$checked + $track': {
|
||||||
|
backgroundColor: green,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
NDAlbumGridView: {
|
NDAlbumGridView: {
|
||||||
albumName: {
|
albumName: {
|
||||||
marginTop: '0.5rem',
|
marginTop: '0.5rem',
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,16 @@ export default {
|
||||||
boxShadow: '3px 3px 5px #3c3836',
|
boxShadow: '3px 3px 5px #3c3836',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiSwitch: {
|
||||||
|
colorSecondary: {
|
||||||
|
'&$checked': {
|
||||||
|
color: '#458588',
|
||||||
|
},
|
||||||
|
'&$checked + $track': {
|
||||||
|
backgroundColor: '#458588',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
NDMobileArtistDetails: {
|
NDMobileArtistDetails: {
|
||||||
bgContainer: {
|
bgContainer: {
|
||||||
background:
|
background:
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,16 @@ export default {
|
||||||
color: `${foreground} !important`,
|
color: `${foreground} !important`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiSwitch: {
|
||||||
|
colorSecondary: {
|
||||||
|
'&$checked': {
|
||||||
|
color: blue,
|
||||||
|
},
|
||||||
|
'&$checked + $track': {
|
||||||
|
backgroundColor: blue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
NDAlbumGridView: {
|
NDAlbumGridView: {
|
||||||
albumName: {
|
albumName: {
|
||||||
marginTop: '0.5rem',
|
marginTop: '0.5rem',
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,16 @@ export default {
|
||||||
color: `${foreground} !important`,
|
color: `${foreground} !important`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiSwitch: {
|
||||||
|
colorSecondary: {
|
||||||
|
'&$checked': {
|
||||||
|
color: blue,
|
||||||
|
},
|
||||||
|
'&$checked + $track': {
|
||||||
|
backgroundColor: blue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
NDAlbumGridView: {
|
NDAlbumGridView: {
|
||||||
albumName: {
|
albumName: {
|
||||||
marginTop: '0.5rem',
|
marginTop: '0.5rem',
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,6 @@ const useCurrentTheme = () => {
|
||||||
...theme.props,
|
...theme.props,
|
||||||
MuiUseMediaQuery: { noSsr: true },
|
MuiUseMediaQuery: { noSsr: true },
|
||||||
MuiPopover: { disableScrollLock: true },
|
MuiPopover: { disableScrollLock: true },
|
||||||
// MUI defaults to secondary, which many themes use as a surface color
|
|
||||||
MuiSwitch: { color: 'primary' },
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[theme],
|
[theme],
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,6 @@ import { Provider } from 'react-redux'
|
||||||
import { createStore } from 'redux'
|
import { createStore } from 'redux'
|
||||||
import mediaQuery from 'css-mediaquery'
|
import mediaQuery from 'css-mediaquery'
|
||||||
import { renderHook } from '@testing-library/react-hooks'
|
import { renderHook } from '@testing-library/react-hooks'
|
||||||
import { render, screen } from '@testing-library/react'
|
|
||||||
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'
|
|
||||||
import Switch from '@material-ui/core/Switch'
|
|
||||||
import themes from './index'
|
|
||||||
import useCurrentTheme from './useCurrentTheme'
|
import useCurrentTheme from './useCurrentTheme'
|
||||||
import { themeReducer } from '../reducers/themeReducer'
|
import { themeReducer } from '../reducers/themeReducer'
|
||||||
import { AUTO_THEME_ID } from '../consts'
|
import { AUTO_THEME_ID } from '../consts'
|
||||||
|
|
@ -165,27 +161,4 @@ describe('useCurrentTheme', () => {
|
||||||
expect(document.body.style.backgroundColor).toBe('rgb(18, 18, 18)')
|
expect(document.body.style.backgroundColor).toBe('rgb(18, 18, 18)')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('switch color', () => {
|
|
||||||
it.each(Object.keys(themes))(
|
|
||||||
'renders switches with the primary color in %s',
|
|
||||||
(theme) => {
|
|
||||||
const { result } = renderHook(() => useCurrentTheme(), {
|
|
||||||
wrapper: ({ children }) => (
|
|
||||||
<Provider store={createStore(themeReducer, { theme })}>
|
|
||||||
{children}
|
|
||||||
</Provider>
|
|
||||||
),
|
|
||||||
})
|
|
||||||
render(
|
|
||||||
<ThemeProvider theme={createMuiTheme(result.current)}>
|
|
||||||
<Switch checked onChange={() => {}} />
|
|
||||||
</ThemeProvider>,
|
|
||||||
)
|
|
||||||
const switchBase = screen
|
|
||||||
.getByRole('checkbox')
|
|
||||||
.closest('.MuiSwitch-switchBase')
|
|
||||||
expect(switchBase.classList).toContain('MuiSwitch-colorPrimary')
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue