Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
f2e73e0
playlist user controls
ajohn723 Feb 13, 2026
75523fe
updated users.ts to incorporate async function feedback
ajohn723 Feb 18, 2026
a1f9632
starting to build hooks and screen for collaborative playlist feature
ajohn723 Feb 19, 2026
b3bf0b7
hook to retrieve lists of users
ajohn723 Mar 3, 2026
dd887f8
Merge branch 'main' of https://github.com/Jellify-Music/App into feat…
ajohn723 Mar 3, 2026
05bcfa1
logic to get playlist users and server users
ajohn723 Mar 7, 2026
f5a1026
changing @/src to ../src
ajohn723 Mar 7, 2026
fdb3787
view for playlist collaborator screen
ajohn723 Mar 9, 2026
f07b5ff
fullscreen modal, filter out current owner of playlist, callbacks for…
ajohn723 Mar 10, 2026
6f5c8ed
adding button to allow share screen to swipe up
ajohn723 Mar 31, 2026
00df1b7
Merge branch 'main' of github.com:Jellify-Music/App into feature/play…
anultravioletaurora Apr 2, 2026
3a4a63a
add scaffold for getUserImageUrl
anultravioletaurora Apr 4, 2026
5096bd1
add additional scaffold
anultravioletaurora Apr 4, 2026
e9f2868
images for users and fixed navigation for share button
ajohn723 Apr 4, 2026
8f73a8c
fix jest
anultravioletaurora Apr 4, 2026
9d0cea5
Merge branch 'main' into feature/playlist-user-operations
anultravioletaurora Apr 4, 2026
4cad4a0
fix imports
anultravioletaurora Apr 4, 2026
0e4f5aa
Merge branch 'feature/playlist-user-operations' of github.com:Jellify…
anultravioletaurora Apr 4, 2026
1748574
update one letter and create unit tests
ajohn723 Apr 4, 2026
f6d3fc1
updated unit test
ajohn723 Apr 4, 2026
5abeeaf
add playlist as param
ajohn723 Apr 4, 2026
e388b44
updating queryClient conventions/types
ajohn723 Apr 6, 2026
9199450
inverted conditional statements for playlist user filtering
ajohn723 Apr 6, 2026
4c6d675
section header and some formatting
ajohn723 Apr 6, 2026
b57096d
updated padding and marginbottom to allow full list of users to be shown
ajohn723 Apr 6, 2026
c0290bd
section header
ajohn723 Apr 6, 2026
a98595a
enable nestedScrolling
ajohn723 Apr 7, 2026
ab607c7
updated more margins, added title
ajohn723 Apr 7, 2026
dfdb2a5
Merge branch 'main' into feature/playlist-user-operations
anultravioletaurora Apr 7, 2026
fc9cd09
lock file
ajohn723 Apr 7, 2026
cc638bc
Merge branch 'main' of github.com:Jellify-Music/App into feature/play…
anultravioletaurora Apr 19, 2026
ffd98c5
sorry aria
anultravioletaurora Apr 19, 2026
5c6d750
restrict playlist share button to playlist owner
ajohn723 Apr 19, 2026
8c85d0b
fixed typo
ajohn723 Apr 19, 2026
d95902a
fix issue where the userId wasn't set when creating a playlist
anultravioletaurora Apr 19, 2026
7ac905d
fix: add playlist users screen presentation
anultravioletaurora Apr 20, 2026
25b0f9b
restore swipe to dismiss on android
anultravioletaurora Apr 20, 2026
25eb334
search bar, baby!
ajohn723 Apr 20, 2026
e378f52
unit testing
ajohn723 Apr 20, 2026
5c9ec9f
Merge branch 'main' into feature/playlist-user-operations
anultravioletaurora Apr 22, 2026
6be0cc2
add post-merge husky hook
anultravioletaurora Apr 22, 2026
e761b03
Merge branch 'main' into feature/playlist-user-operations
anultravioletaurora Apr 30, 2026
c25d4ed
Merge branch 'main' into feature/playlist-user-operations
anultravioletaurora Apr 30, 2026
5ca141b
Merge branch 'main' into feature/playlist-user-operations
anultravioletaurora May 1, 2026
aacae70
Merge branch 'main' into feature/playlist-user-operations
anultravioletaurora May 2, 2026
61ed1fe
apply prettier
anultravioletaurora May 2, 2026
6543296
Merge branch 'main' of github.com:Jellify-Music/App into feature/play…
anultravioletaurora May 12, 2026
2e542f2
Merge branch 'main' into feature/playlist-user-operations
anultravioletaurora May 22, 2026
efa2bfb
Fix build
anultravioletaurora May 22, 2026
70f23b5
Merge branch 'main' into feature/playlist-user-operations
anultravioletaurora May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .husky/post-merge
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if git diff-tree --no-commit-id -r --name-only ORIG_HEAD HEAD | grep -qE "package\.json|bun\.lockb"; then
echo "📦 Dependencies changed, running module install..."
bun i
echo "✅ Module install completed, ready to rock 🪨!"
fi
299 changes: 299 additions & 0 deletions jest/functional/playlist-users.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
import { getApi } from '../../src/stores/auth/utils'
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api'
import {
addPlaylistUser,
getPlaylistUsers,
removePlaylistUser,
} from '../../src/api/queries/playlist/utils/users'
import { BaseItemDto, PlaylistUserPermissions, UserDto } from '@jellyfin/sdk/lib/generated-client'

jest.mock('../../src/stores')
jest.mock('@jellyfin/sdk/lib/utils/api')

describe('Playlist Users API Functions', () => {
const mockPlaylistId = 'playlist-123'
const mockUserId = 'user-456'
const mockApi = { basePath: 'http://test' }

beforeEach(() => {
jest.clearAllMocks()
})

describe('getPlaylistUsers', () => {
it('fetches playlist users successfully', async () => {
const mockUsers: PlaylistUserPermissions[] = [
{ UserId: 'user-1', CanEdit: true },
{ UserId: 'user-2', CanEdit: false },
]

const mockPlaylistApi = {
getPlaylistUsers: jest.fn().mockResolvedValue({ data: mockUsers }),
}

;(getApi as jest.Mock).mockReturnValue(mockApi)
;(getPlaylistsApi as jest.Mock).mockReturnValue(mockPlaylistApi)

const result = await getPlaylistUsers(mockPlaylistId)

expect(result).toEqual(mockUsers)
expect(getPlaylistsApi).toHaveBeenCalledWith(mockApi)
expect(mockPlaylistApi.getPlaylistUsers).toHaveBeenCalledWith({
playlistId: mockPlaylistId,
})
})

it('throws error when API instance is not set', async () => {
;(getApi as jest.Mock).mockReturnValue(null)

await expect(getPlaylistUsers(mockPlaylistId)).rejects.toThrow('API Instance not set')
})
})

describe('addPlaylistUser', () => {
it('adds a user to playlist with correct permissions', async () => {
const mockPlaylistApi = {
updatePlaylist: jest.fn().mockResolvedValue({}),
}

;(getApi as jest.Mock).mockReturnValue(mockApi)
;(getPlaylistsApi as jest.Mock).mockReturnValue(mockPlaylistApi)

await addPlaylistUser(mockPlaylistId, mockUserId, true)

expect(mockPlaylistApi.updatePlaylist).toHaveBeenCalledWith({
playlistId: mockPlaylistId,
updatePlaylistDto: {
Users: [
{
UserId: mockUserId,
CanEdit: true,
},
],
},
})
})

it('adds a user to playlist with read-only permission', async () => {
const mockPlaylistApi = {
updatePlaylist: jest.fn().mockResolvedValue({}),
}

;(getApi as jest.Mock).mockReturnValue(mockApi)
;(getPlaylistsApi as jest.Mock).mockReturnValue(mockPlaylistApi)

await addPlaylistUser(mockPlaylistId, mockUserId, false)

expect(mockPlaylistApi.updatePlaylist).toHaveBeenCalledWith({
playlistId: mockPlaylistId,
updatePlaylistDto: {
Users: [
{
UserId: mockUserId,
CanEdit: false,
},
],
},
})
})
})

describe('removePlaylistUser', () => {
it('removes a user from playlist', async () => {
const mockPlaylistApi = {
removeUserFromPlaylist: jest.fn().mockResolvedValue({}),
}

;(getApi as jest.Mock).mockReturnValue(mockApi)
;(getPlaylistsApi as jest.Mock).mockReturnValue(mockPlaylistApi)

await removePlaylistUser(mockPlaylistId, mockUserId)

expect(mockPlaylistApi.removeUserFromPlaylist).toHaveBeenCalledWith({
playlistId: mockPlaylistId,
userId: mockUserId,
})
})
})
})

describe('Playlist Users Query Client Updates', () => {
const mockPlaylist: BaseItemDto = {
Id: 'playlist-123',
Name: 'Test Playlist',
Type: 'Playlist',
}

const mockUser: UserDto = {
Id: 'user-456',
Name: 'Test User',
}

describe('useAddPlaylistUser onSuccess', () => {
it('should add new user to empty playlist users cache', () => {
const previousData: PlaylistUserPermissions[] | undefined = undefined
const newUser: PlaylistUserPermissions = {
UserId: mockUser.Id,
CanEdit: true,
}

// Simulate the query update function
const updateFn = (previous: PlaylistUserPermissions[] | undefined) => {
if (previous == undefined) {
return [newUser]
} else {
return [...previous, newUser]
}
}

const result = updateFn(previousData)
expect(result).toEqual([newUser])
expect(result).toHaveLength(1)
})

it('should add new user to existing playlist users list', () => {
const existingUser: PlaylistUserPermissions = {
UserId: 'user-existing',
CanEdit: true,
}
const previousData: PlaylistUserPermissions[] = [existingUser]
const newUser: PlaylistUserPermissions = {
UserId: mockUser.Id,
CanEdit: false,
}

// Simulate the query update function
const updateFn = (previous: PlaylistUserPermissions[] | undefined) => {
if (previous == undefined) {
return [newUser]
} else {
return [...previous, newUser]
}
}

const result = updateFn(previousData)
expect(result).toEqual([existingUser, newUser])
expect(result).toHaveLength(2)
})

it('should respect CanEdit permission when adding user', () => {
const previousData: PlaylistUserPermissions[] | undefined = undefined
const canEditValue = false
const newUser: PlaylistUserPermissions = {
UserId: mockUser.Id,
CanEdit: canEditValue,
}

// Simulate the query update function
const updateFn = (previous: PlaylistUserPermissions[] | undefined) => {
if (previous == undefined) {
return [newUser]
} else {
return [...previous, newUser]
}
}

const result = updateFn(previousData)
expect(result[0].CanEdit).toBe(false)
})
})

describe('useRemovePlaylistUser onSuccess', () => {
it('should remove user from playlist users cache', () => {
const userToRemove = 'user-456'
const previousData: PlaylistUserPermissions[] = [
{ UserId: 'user-1', CanEdit: true },
{ UserId: userToRemove, CanEdit: false },
{ UserId: 'user-3', CanEdit: true },
]

// Simulate the query update function
const updateFn = (previous: PlaylistUserPermissions[] | undefined) => {
if (previous == undefined) {
return []
} else {
return previous.filter((user) => user.UserId != userToRemove)
}
}

const result = updateFn(previousData)
expect(result).toEqual([
{ UserId: 'user-1', CanEdit: true },
{ UserId: 'user-3', CanEdit: true },
])
expect(result).toHaveLength(2)
})

it('should handle removing from empty list', () => {
const userToRemove = 'user-456'
const previousData: PlaylistUserPermissions[] | undefined = undefined

// Simulate the query update function
const updateFn = (previous: PlaylistUserPermissions[] | undefined) => {
if (previous == undefined) {
return []
} else {
return previous.filter((user) => user.UserId != userToRemove)
}
}

const result = updateFn(previousData)
expect(result).toEqual([])
expect(result).toHaveLength(0)
})

it('should handle removing non-existent user gracefully', () => {
const userToRemove = 'user-nonexistent'
const previousData: PlaylistUserPermissions[] = [
{ UserId: 'user-1', CanEdit: true },
{ UserId: 'user-2', CanEdit: false },
]

// Simulate the query update function
const updateFn = (previous: PlaylistUserPermissions[] | undefined) => {
if (previous == undefined) {
return []
} else {
return previous.filter((user) => user.UserId != userToRemove)
}
}

const result = updateFn(previousData)
expect(result).toEqual(previousData)
expect(result).toHaveLength(2)
})
})

describe('Mutation Variables Validation', () => {
it('should use correct CanEdit value from mutation variables', () => {
const variables = {
playlist: mockPlaylist,
user: mockUser,
CanEdit: false,
}

// Simulate adding with the CanEdit value from variables
const newUser: PlaylistUserPermissions = {
UserId: variables.user.Id,
CanEdit: variables.CanEdit,
}

expect(newUser.CanEdit).toBe(false)
expect(newUser.UserId).toBe(mockUser.Id)
})

it('should maintain user ID from variables in cache', () => {
const variables = {
playlist: mockPlaylist,
user: mockUser,
CanEdit: true,
}

const newUser: PlaylistUserPermissions = {
UserId: variables.user.Id,
CanEdit: variables.CanEdit,
}

expect(newUser.UserId).toBe('user-456')
})
})
})
1 change: 1 addition & 0 deletions src/api/mutations/playlist/utils/playlists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export async function createPlaylist(name: string) {
Name: name,
IsPublic: false,
MediaType: MediaType.Audio,
UserId: user.id,
},
})
.then((result) => {
Expand Down
Loading
Loading