mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Compare commits
1 Commits
813b359575
...
6f7caf7df9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f7caf7df9 |
2
Makefile
2
Makefile
@ -54,7 +54,7 @@ testall: test-race test-i18n test-js ##@Development Run Go and JS tests
|
|||||||
.PHONY: testall
|
.PHONY: testall
|
||||||
|
|
||||||
test-race: ##@Development Run Go tests with race detector
|
test-race: ##@Development Run Go tests with race detector
|
||||||
go test -tags netgo -race -shuffle=on $(PKG)
|
go test -tags netgo -race -shuffle=on ./...
|
||||||
.PHONY: test-race
|
.PHONY: test-race
|
||||||
|
|
||||||
test-js: ##@Development Run JS tests
|
test-js: ##@Development Run JS tests
|
||||||
|
|||||||
@ -166,7 +166,7 @@ func (s *maintenanceService) getAffectedAlbumIDs(ctx context.Context, ids []stri
|
|||||||
if len(ids) > 0 {
|
if len(ids) > 0 {
|
||||||
filters = squirrel.And{
|
filters = squirrel.And{
|
||||||
squirrel.Eq{"missing": true},
|
squirrel.Eq{"missing": true},
|
||||||
squirrel.Eq{"media_file.id": ids},
|
squirrel.Eq{"id": ids},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/core/agents"
|
"github.com/navidrome/navidrome/core/agents"
|
||||||
@ -23,11 +22,8 @@ var _ = Describe("Plugin Manager", func() {
|
|||||||
// but, as this is an integration test, we can't use configtest.SetupConfig() as it causes
|
// but, as this is an integration test, we can't use configtest.SetupConfig() as it causes
|
||||||
// data races.
|
// data races.
|
||||||
originalPluginsFolder := conf.Server.Plugins.Folder
|
originalPluginsFolder := conf.Server.Plugins.Folder
|
||||||
originalTimeout := conf.Server.DevPluginCompilationTimeout
|
|
||||||
conf.Server.DevPluginCompilationTimeout = 2 * time.Minute
|
|
||||||
DeferCleanup(func() {
|
DeferCleanup(func() {
|
||||||
conf.Server.Plugins.Folder = originalPluginsFolder
|
conf.Server.Plugins.Folder = originalPluginsFolder
|
||||||
conf.Server.DevPluginCompilationTimeout = originalTimeout
|
|
||||||
})
|
})
|
||||||
conf.Server.Plugins.Enabled = true
|
conf.Server.Plugins.Enabled = true
|
||||||
conf.Server.Plugins.Folder = testDataDir
|
conf.Server.Plugins.Folder = testDataDir
|
||||||
|
|||||||
@ -12,21 +12,7 @@ const isAdmin = () => {
|
|||||||
const getSelectedLibraries = () => {
|
const getSelectedLibraries = () => {
|
||||||
try {
|
try {
|
||||||
const state = JSON.parse(localStorage.getItem('state'))
|
const state = JSON.parse(localStorage.getItem('state'))
|
||||||
const selectedLibraries = state?.library?.selectedLibraries || []
|
return state?.library?.selectedLibraries || []
|
||||||
const userLibraries = state?.library?.userLibraries || []
|
|
||||||
|
|
||||||
// Validate selected libraries against current user libraries
|
|
||||||
const userLibraryIds = userLibraries.map((lib) => lib.id)
|
|
||||||
const validatedSelection = selectedLibraries.filter((id) =>
|
|
||||||
userLibraryIds.includes(id),
|
|
||||||
)
|
|
||||||
|
|
||||||
// If user has only one library, return empty array (no filter needed)
|
|
||||||
if (userLibraryIds.length === 1) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return validatedSelection
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { cloneElement } from 'react'
|
import React, { cloneElement } from 'react'
|
||||||
import { sanitizeListRestProps, TopToolbar, CreateButton } from 'react-admin'
|
import { sanitizeListRestProps, TopToolbar } from 'react-admin'
|
||||||
import LibraryScanButton from './LibraryScanButton'
|
import LibraryScanButton from './LibraryScanButton'
|
||||||
|
|
||||||
const LibraryListActions = ({
|
const LibraryListActions = ({
|
||||||
@ -23,7 +23,6 @@ const LibraryListActions = ({
|
|||||||
})}
|
})}
|
||||||
<LibraryScanButton fullScan={false} />
|
<LibraryScanButton fullScan={false} />
|
||||||
<LibraryScanButton fullScan={true} />
|
<LibraryScanButton fullScan={true} />
|
||||||
<CreateButton />
|
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,39 +8,18 @@ const initialState = {
|
|||||||
export const libraryReducer = (previousState = initialState, payload) => {
|
export const libraryReducer = (previousState = initialState, payload) => {
|
||||||
const { type, data } = payload
|
const { type, data } = payload
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SET_USER_LIBRARIES: {
|
case SET_USER_LIBRARIES:
|
||||||
const newUserLibraryIds = data.map((lib) => lib.id)
|
|
||||||
|
|
||||||
// Validate and filter selected libraries to only include IDs that exist in new user libraries
|
|
||||||
const validatedSelection = previousState.selectedLibraries.filter((id) =>
|
|
||||||
newUserLibraryIds.includes(id),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Determine the final selection:
|
|
||||||
// 1. If first time setting libraries (no previous user libraries), select all
|
|
||||||
// 2. If user now has only one library, reset to empty (no filter needed)
|
|
||||||
// 3. Otherwise, use validated selection (may be empty if all previous selections were invalid)
|
|
||||||
let finalSelection
|
|
||||||
if (
|
|
||||||
previousState.selectedLibraries.length === 0 &&
|
|
||||||
previousState.userLibraries.length === 0
|
|
||||||
) {
|
|
||||||
// First time: select all libraries
|
|
||||||
finalSelection = newUserLibraryIds
|
|
||||||
} else if (newUserLibraryIds.length === 1) {
|
|
||||||
// Single library: reset selection (empty means "all accessible")
|
|
||||||
finalSelection = []
|
|
||||||
} else {
|
|
||||||
// Multiple libraries: use validated selection
|
|
||||||
finalSelection = validatedSelection
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...previousState,
|
...previousState,
|
||||||
userLibraries: data,
|
userLibraries: data,
|
||||||
selectedLibraries: finalSelection,
|
// If this is the first time setting user libraries and no selection exists,
|
||||||
|
// default to all libraries
|
||||||
|
selectedLibraries:
|
||||||
|
previousState.selectedLibraries.length === 0 &&
|
||||||
|
previousState.userLibraries.length === 0
|
||||||
|
? data.map((lib) => lib.id)
|
||||||
|
: previousState.selectedLibraries,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case SET_SELECTED_LIBRARIES:
|
case SET_SELECTED_LIBRARIES:
|
||||||
return {
|
return {
|
||||||
...previousState,
|
...previousState,
|
||||||
|
|||||||
@ -1,186 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
import { libraryReducer } from './libraryReducer'
|
|
||||||
import { SET_SELECTED_LIBRARIES, SET_USER_LIBRARIES } from '../actions'
|
|
||||||
|
|
||||||
describe('libraryReducer', () => {
|
|
||||||
const mockLibraries = [
|
|
||||||
{ id: '1', name: 'Music Library' },
|
|
||||||
{ id: '2', name: 'Podcasts' },
|
|
||||||
{ id: '3', name: 'Audiobooks' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
userLibraries: [],
|
|
||||||
selectedLibraries: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('SET_USER_LIBRARIES', () => {
|
|
||||||
it('should set user libraries and select all on first load', () => {
|
|
||||||
const action = {
|
|
||||||
type: SET_USER_LIBRARIES,
|
|
||||||
data: mockLibraries,
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = libraryReducer(initialState, action)
|
|
||||||
|
|
||||||
expect(result.userLibraries).toEqual(mockLibraries)
|
|
||||||
expect(result.selectedLibraries).toEqual(['1', '2', '3'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should reset selection to empty when user has only one library', () => {
|
|
||||||
const previousState = {
|
|
||||||
userLibraries: mockLibraries,
|
|
||||||
selectedLibraries: ['1', '2'],
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = {
|
|
||||||
type: SET_USER_LIBRARIES,
|
|
||||||
data: [mockLibraries[0]], // Only one library now
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = libraryReducer(previousState, action)
|
|
||||||
|
|
||||||
expect(result.userLibraries).toEqual([mockLibraries[0]])
|
|
||||||
expect(result.selectedLibraries).toEqual([]) // Reset for single library
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should filter out invalid library IDs from selection', () => {
|
|
||||||
const previousState = {
|
|
||||||
userLibraries: mockLibraries,
|
|
||||||
selectedLibraries: ['1', '2', '3'],
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = {
|
|
||||||
type: SET_USER_LIBRARIES,
|
|
||||||
data: [mockLibraries[0], mockLibraries[1]], // Only libraries 1 and 2 remain
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = libraryReducer(previousState, action)
|
|
||||||
|
|
||||||
expect(result.userLibraries).toEqual([mockLibraries[0], mockLibraries[1]])
|
|
||||||
expect(result.selectedLibraries).toEqual(['1', '2']) // Library 3 removed
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should keep valid selection when libraries change', () => {
|
|
||||||
const previousState = {
|
|
||||||
userLibraries: mockLibraries,
|
|
||||||
selectedLibraries: ['1'],
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = {
|
|
||||||
type: SET_USER_LIBRARIES,
|
|
||||||
data: mockLibraries, // Same libraries
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = libraryReducer(previousState, action)
|
|
||||||
|
|
||||||
expect(result.userLibraries).toEqual(mockLibraries)
|
|
||||||
expect(result.selectedLibraries).toEqual(['1']) // Selection preserved
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle selection becoming empty after filtering invalid IDs', () => {
|
|
||||||
const previousState = {
|
|
||||||
userLibraries: mockLibraries,
|
|
||||||
selectedLibraries: ['1', '2'],
|
|
||||||
}
|
|
||||||
|
|
||||||
const newLibraries = [{ id: '4', name: 'New Library' }]
|
|
||||||
const action = {
|
|
||||||
type: SET_USER_LIBRARIES,
|
|
||||||
data: newLibraries,
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = libraryReducer(previousState, action)
|
|
||||||
|
|
||||||
expect(result.userLibraries).toEqual(newLibraries)
|
|
||||||
expect(result.selectedLibraries).toEqual([]) // All selected IDs were invalid
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle transition from multiple to single library with invalid selection', () => {
|
|
||||||
const previousState = {
|
|
||||||
userLibraries: mockLibraries,
|
|
||||||
selectedLibraries: ['2', '3'], // User had libraries 2 and 3 selected
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = {
|
|
||||||
type: SET_USER_LIBRARIES,
|
|
||||||
data: [mockLibraries[0]], // Now only has access to library 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = libraryReducer(previousState, action)
|
|
||||||
|
|
||||||
expect(result.userLibraries).toEqual([mockLibraries[0]])
|
|
||||||
expect(result.selectedLibraries).toEqual([]) // Reset for single library
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle empty library list', () => {
|
|
||||||
const previousState = {
|
|
||||||
userLibraries: mockLibraries,
|
|
||||||
selectedLibraries: ['1', '2'],
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = {
|
|
||||||
type: SET_USER_LIBRARIES,
|
|
||||||
data: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = libraryReducer(previousState, action)
|
|
||||||
|
|
||||||
expect(result.userLibraries).toEqual([])
|
|
||||||
expect(result.selectedLibraries).toEqual([]) // All selections filtered out
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('SET_SELECTED_LIBRARIES', () => {
|
|
||||||
it('should update selected libraries', () => {
|
|
||||||
const previousState = {
|
|
||||||
userLibraries: mockLibraries,
|
|
||||||
selectedLibraries: ['1'],
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = {
|
|
||||||
type: SET_SELECTED_LIBRARIES,
|
|
||||||
data: ['2', '3'],
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = libraryReducer(previousState, action)
|
|
||||||
|
|
||||||
expect(result.selectedLibraries).toEqual(['2', '3'])
|
|
||||||
expect(result.userLibraries).toEqual(mockLibraries) // Unchanged
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should allow setting empty selection', () => {
|
|
||||||
const previousState = {
|
|
||||||
userLibraries: mockLibraries,
|
|
||||||
selectedLibraries: ['1', '2'],
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = {
|
|
||||||
type: SET_SELECTED_LIBRARIES,
|
|
||||||
data: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = libraryReducer(previousState, action)
|
|
||||||
|
|
||||||
expect(result.selectedLibraries).toEqual([])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('unknown action', () => {
|
|
||||||
it('should return previous state for unknown action', () => {
|
|
||||||
const previousState = {
|
|
||||||
userLibraries: mockLibraries,
|
|
||||||
selectedLibraries: ['1'],
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = {
|
|
||||||
type: 'UNKNOWN_ACTION',
|
|
||||||
data: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = libraryReducer(previousState, action)
|
|
||||||
|
|
||||||
expect(result).toBe(previousState) // Same reference
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Loading…
x
Reference in New Issue
Block a user