Add create/update playlist mutations and form

This commit is contained in:
jeffvli 2022-12-31 12:40:11 -08:00
parent 82f107d835
commit 88f53c17db
11 changed files with 409 additions and 145 deletions

View file

@ -33,6 +33,8 @@ import type {
PlaylistSongListArgs, PlaylistSongListArgs,
ArtistListArgs, ArtistListArgs,
RawArtistListResponse, RawArtistListResponse,
UpdatePlaylistArgs,
RawUpdatePlaylistResponse,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { subsonicApi } from '/@/renderer/api/subsonic.api'; import { subsonicApi } from '/@/renderer/api/subsonic.api';
import { jellyfinApi } from '/@/renderer/api/jellyfin.api'; import { jellyfinApi } from '/@/renderer/api/jellyfin.api';
@ -60,7 +62,7 @@ export type ControllerEndpoint = Partial<{
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<RawSongListResponse>; getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<RawSongListResponse>;
getSongDetail: (args: SongDetailArgs) => Promise<RawSongDetailResponse>; getSongDetail: (args: SongDetailArgs) => Promise<RawSongDetailResponse>;
getSongList: (args: SongListArgs) => Promise<RawSongListResponse>; getSongList: (args: SongListArgs) => Promise<RawSongListResponse>;
updatePlaylist: () => void; updatePlaylist: (args: UpdatePlaylistArgs) => Promise<RawUpdatePlaylistResponse>;
updateRating: (args: RatingArgs) => Promise<RawRatingResponse>; updateRating: (args: RatingArgs) => Promise<RawRatingResponse>;
}>; }>;
@ -120,7 +122,7 @@ const endpoints: ApiController = {
getPlaylistSongList: navidromeApi.getPlaylistSongList, getPlaylistSongList: navidromeApi.getPlaylistSongList,
getSongDetail: navidromeApi.getSongDetail, getSongDetail: navidromeApi.getSongDetail,
getSongList: navidromeApi.getSongList, getSongList: navidromeApi.getSongList,
updatePlaylist: undefined, updatePlaylist: navidromeApi.updatePlaylist,
updateRating: subsonicApi.updateRating, updateRating: subsonicApi.updateRating,
}, },
subsonic: { subsonic: {
@ -203,7 +205,16 @@ const getPlaylistList = async (args: PlaylistListArgs) => {
return (apiController('getPlaylistList') as ControllerEndpoint['getPlaylistList'])?.(args); return (apiController('getPlaylistList') as ControllerEndpoint['getPlaylistList'])?.(args);
}; };
const createPlaylist = async (args: CreatePlaylistArgs) => {
return (apiController('createPlaylist') as ControllerEndpoint['createPlaylist'])?.(args);
};
const updatePlaylist = async (args: UpdatePlaylistArgs) => {
return (apiController('updatePlaylist') as ControllerEndpoint['updatePlaylist'])?.(args);
};
export const controller = { export const controller = {
createPlaylist,
getAlbumArtistList, getAlbumArtistList,
getAlbumDetail, getAlbumDetail,
getAlbumList, getAlbumList,
@ -212,4 +223,5 @@ export const controller = {
getMusicFolderList, getMusicFolderList,
getPlaylistList, getPlaylistList,
getSongList, getSongList,
updatePlaylist,
}; };

View file

@ -32,6 +32,8 @@ import type {
NDSongListResponse, NDSongListResponse,
NDAlbumArtist, NDAlbumArtist,
NDPlaylist, NDPlaylist,
NDUpdatePlaylistParams,
NDUpdatePlaylistResponse,
} from '/@/renderer/api/navidrome.types'; } from '/@/renderer/api/navidrome.types';
import { NDPlaylistListSort, NDSongListSort, NDSortOrder } from '/@/renderer/api/navidrome.types'; import { NDPlaylistListSort, NDSongListSort, NDSortOrder } from '/@/renderer/api/navidrome.types';
import type { import type {
@ -53,6 +55,8 @@ import type {
PlaylistSongListArgs, PlaylistSongListArgs,
AlbumArtist, AlbumArtist,
Playlist, Playlist,
UpdatePlaylistResponse,
UpdatePlaylistArgs,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { import {
playlistListSortMap, playlistListSortMap,
@ -286,7 +290,7 @@ const getSongDetail = async (args: SongDetailArgs): Promise<NDSongDetail> => {
}; };
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => { const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
const { query, server, signal } = args; const { query, server } = args;
const json: NDCreatePlaylistParams = { const json: NDCreatePlaylistParams = {
comment: query.comment, comment: query.comment,
@ -299,7 +303,6 @@ const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistR
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
json, json,
prefixUrl: server?.url, prefixUrl: server?.url,
signal,
}) })
.json<NDCreatePlaylistResponse>(); .json<NDCreatePlaylistResponse>();
@ -309,6 +312,33 @@ const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistR
}; };
}; };
const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistResponse> => {
const { query, server, signal } = args;
const previous = query.previous as NDPlaylist;
const json: NDUpdatePlaylistParams = {
...previous,
comment: query.comment || '',
name: query.name,
public: query.public || false,
};
const data = await api
.post(`api/playlist/${previous.id}`, {
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
json,
prefixUrl: server?.url,
signal,
})
.json<NDUpdatePlaylistResponse>();
return {
id: data.id,
name: query.name,
};
};
const deletePlaylist = async (args: DeletePlaylistArgs): Promise<NDDeletePlaylist> => { const deletePlaylist = async (args: DeletePlaylistArgs): Promise<NDDeletePlaylist> => {
const { query, server, signal } = args; const { query, server, signal } = args;
@ -552,6 +582,7 @@ export const navidromeApi = {
getPlaylistSongList, getPlaylistSongList,
getSongDetail, getSongDetail,
getSongList, getSongList,
updatePlaylist,
}; };
export const ndNormalize = { export const ndNormalize = {

View file

@ -259,7 +259,8 @@ export type NDAlbumArtistListParams = {
export type NDCreatePlaylistParams = { export type NDCreatePlaylistParams = {
comment?: string; comment?: string;
name: string; name: string;
public: boolean; public?: boolean;
rules?: Record<string, any> | null;
}; };
export type NDCreatePlaylistResponse = { export type NDCreatePlaylistResponse = {
@ -268,6 +269,10 @@ export type NDCreatePlaylistResponse = {
export type NDCreatePlaylist = NDCreatePlaylistResponse; export type NDCreatePlaylist = NDCreatePlaylistResponse;
export type NDUpdatePlaylistParams = NDPlaylist;
export type NDUpdatePlaylistResponse = NDPlaylist;
export type NDDeletePlaylistParams = { export type NDDeletePlaylistParams = {
id: string; id: string;
}; };

View file

@ -120,16 +120,6 @@ export interface BasePaginatedResponse<T> {
totalRecordCount: number; totalRecordCount: number;
} }
export type ApiError = {
error: {
message: string;
path: string;
trace: string[];
};
response: string;
statusCode: number;
};
export type AuthenticationResponse = { export type AuthenticationResponse = {
credential: string; credential: string;
ndCredential?: string; ndCredential?: string;
@ -740,10 +730,30 @@ export type RawCreatePlaylistResponse = CreatePlaylistResponse | undefined;
export type CreatePlaylistResponse = { id: string; name: string }; export type CreatePlaylistResponse = { id: string; name: string };
export type CreatePlaylistQuery = { comment?: string; name: string; public?: boolean }; export type CreatePlaylistQuery = {
comment?: string;
name: string;
public?: boolean;
rules?: Record<string, any>;
};
export type CreatePlaylistArgs = { query: CreatePlaylistQuery } & BaseEndpointArgs; export type CreatePlaylistArgs = { query: CreatePlaylistQuery } & BaseEndpointArgs;
// Update Playlist
export type RawUpdatePlaylistResponse = UpdatePlaylistResponse | undefined;
export type UpdatePlaylistResponse = { id: string; name: string };
export type UpdatePlaylistQuery = {
comment?: string;
name: string;
previous: RawPlaylistDetailResponse;
public?: boolean;
rules?: Record<string, any>;
};
export type UpdatePlaylistArgs = { query: UpdatePlaylistQuery } & BaseEndpointArgs;
// Delete Playlist // Delete Playlist
export type RawDeletePlaylistResponse = NDDeletePlaylist | undefined; export type RawDeletePlaylistResponse = NDDeletePlaylist | undefined;

View file

@ -0,0 +1,76 @@
import { Group, Stack } from '@mantine/core';
import { useForm } from '@mantine/form';
import { CreatePlaylistQuery } from '/@/renderer/api/types';
import { Button, Switch, TextInput, toast } from '/@/renderer/components';
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
interface CreatePlaylistFormProps {
onCancel: () => void;
}
export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
const mutation = useCreatePlaylist();
const form = useForm<CreatePlaylistQuery>({
initialValues: {
comment: '',
name: '',
public: false,
rules: undefined,
},
});
const handleSubmit = form.onSubmit((values) => {
mutation.mutate(
{ query: values },
{
onError: (err) => {
toast.error({ message: err.message, title: 'Error creating playlist' });
},
onSuccess: () => {
toast.success({ message: 'Playlist created successfully' });
onCancel();
},
},
);
});
const isSubmitDisabled = !form.values.name || mutation.isLoading;
return (
<form onSubmit={handleSubmit}>
<Stack>
<TextInput
data-autofocus
required
label="Name"
{...form.getInputProps('name')}
/>
<TextInput
label="Description"
{...form.getInputProps('comment')}
/>
<Switch
label="Is Public?"
{...form.getInputProps('public')}
/>
</Stack>
<Group position="right">
<Button
variant="subtle"
onClick={onCancel}
>
Cancel
</Button>
<Button
disabled={isSubmitDisabled}
loading={mutation.isLoading}
type="submit"
variant="filled"
>
Save
</Button>
</Group>
</form>
);
};

View file

@ -1 +1,2 @@
export * from './queries/playlist-list-query'; export * from './queries/playlist-list-query';
export * from './components/create-playlist-form';

View file

@ -0,0 +1,25 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { HTTPError } from 'ky';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { CreatePlaylistArgs, RawCreatePlaylistResponse } from '/@/renderer/api/types';
import { MutationOptions } from '/@/renderer/lib/react-query';
import { useCurrentServer } from '/@/renderer/store';
export const useCreatePlaylist = (options?: MutationOptions) => {
const queryClient = useQueryClient();
const server = useCurrentServer();
return useMutation<
RawCreatePlaylistResponse,
HTTPError,
Omit<CreatePlaylistArgs, 'server'>,
null
>({
mutationFn: (args) => api.controller.createPlaylist({ ...args, server }),
onSuccess: () => {
queryClient.invalidateQueries(queryKeys.playlists.list(server?.id || ''));
},
...options,
});
};

View file

@ -0,0 +1,29 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { HTTPError } from 'ky';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { RawUpdatePlaylistResponse, UpdatePlaylistArgs } from '/@/renderer/api/types';
import { MutationOptions } from '/@/renderer/lib/react-query';
import { useCurrentServer } from '/@/renderer/store';
export const useUpdatePlaylist = (options?: MutationOptions) => {
const queryClient = useQueryClient();
const server = useCurrentServer();
return useMutation<
RawUpdatePlaylistResponse,
HTTPError,
Omit<UpdatePlaylistArgs, 'server'>,
null
>({
mutationFn: (args) => api.controller.updatePlaylist({ ...args, server }),
onSuccess: (data) => {
queryClient.invalidateQueries(queryKeys.playlists.list(server?.id || ''));
if (data?.id) {
queryClient.invalidateQueries(queryKeys.playlists.detail(server?.id || '', data.id));
}
},
...options,
});
};

View file

@ -1,16 +1,16 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { createPolymorphicComponent, Flex, FlexProps } from '@mantine/core';
import type { LinkProps } from 'react-router-dom'; import type { LinkProps } from 'react-router-dom';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
interface ListItemProps { interface ListItemProps extends FlexProps {
children: ReactNode; children: ReactNode;
disabled?: boolean; disabled?: boolean;
to?: string; to?: string;
} }
const StyledItem = styled.div` const StyledItem = styled(Flex)`
display: flex;
width: 100%; width: 100%;
font-family: var(--content-font-family); font-family: var(--content-font-family);
@ -32,11 +32,7 @@ const ItemStyle = css`
} }
`; `;
const Box = styled.div` const _ItemLink = styled(StyledItem)<LinkProps & { disabled?: boolean }>`
${ItemStyle}
`;
const ItemLink = styled(Link)<LinkProps & { disabled?: boolean }>`
opacity: ${(props) => props.disabled && 0.6}; opacity: ${(props) => props.disabled && 0.6};
pointer-events: ${(props) => props.disabled && 'none'}; pointer-events: ${(props) => props.disabled && 'none'};
@ -47,12 +43,15 @@ const ItemLink = styled(Link)<LinkProps & { disabled?: boolean }>`
${ItemStyle} ${ItemStyle}
`; `;
export const SidebarItem = ({ to, children, ...rest }: ListItemProps) => { const ItemLink = createPolymorphicComponent<'a', ListItemProps>(_ItemLink);
export const SidebarItem = ({ to, children, ...props }: ListItemProps) => {
if (to) { if (to) {
return ( return (
<ItemLink <ItemLink
component={Link}
to={to} to={to}
{...rest} {...props}
> >
{children} {children}
</ItemLink> </ItemLink>
@ -61,15 +60,13 @@ export const SidebarItem = ({ to, children, ...rest }: ListItemProps) => {
return ( return (
<StyledItem <StyledItem
tabIndex={0} tabIndex={0}
{...rest} {...props}
> >
{children} {children}
</StyledItem> </StyledItem>
); );
}; };
SidebarItem.Box = Box;
SidebarItem.Link = ItemLink; SidebarItem.Link = ItemLink;
SidebarItem.defaultProps = { SidebarItem.defaultProps = {

View file

@ -1,9 +1,12 @@
import { MouseEvent } from 'react';
import { Stack, Grid, Accordion, Center, Group } from '@mantine/core'; import { Stack, Grid, Accordion, Center, Group } from '@mantine/core';
import { closeAllModals, openModal } from '@mantine/modals';
import { SpotlightProvider } from '@mantine/spotlight'; import { SpotlightProvider } from '@mantine/spotlight';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { BsCollection } from 'react-icons/bs'; import { BsCollection } from 'react-icons/bs';
import { Button, TextInput } from '/@/renderer/components'; import { Button, ScrollArea, TextInput } from '/@/renderer/components';
import { import {
RiAddFill,
RiAlbumFill, RiAlbumFill,
RiAlbumLine, RiAlbumLine,
RiArrowDownSLine, RiArrowDownSLine,
@ -16,22 +19,26 @@ import {
RiFolder3Line, RiFolder3Line,
RiHome5Fill, RiHome5Fill,
RiHome5Line, RiHome5Line,
RiMenuUnfoldLine,
RiMusicFill, RiMusicFill,
RiMusicLine, RiMusicLine,
RiPlayListLine, RiPlayListLine,
RiSearchLine, RiSearchLine,
RiUserVoiceFill,
RiUserVoiceLine, RiUserVoiceLine,
} from 'react-icons/ri'; } from 'react-icons/ri';
import { useNavigate, Link, useLocation } from 'react-router-dom'; import { useNavigate, Link, useLocation, generatePath } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item'; import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useSidebarStore, useAppStoreActions, useCurrentSong } from '/@/renderer/store'; import { useSidebarStore, useAppStoreActions, useCurrentSong } from '/@/renderer/store';
import { fadeIn } from '/@/renderer/styles'; import { fadeIn } from '/@/renderer/styles';
import { CreatePlaylistForm, usePlaylistList } from '/@/renderer/features/playlists';
import { PlaylistListSort, SortOrder } from '/@/renderer/api/types';
const SidebarContainer = styled.div` const SidebarContainer = styled.div`
height: 100%; height: 100%;
max-height: calc(100vh - 85px); // Account for and playerbar max-height: calc(100vh - 85px); // Account for playerbar
user-select: none; user-select: none;
`; `;
@ -68,20 +75,35 @@ export const Sidebar = () => {
const sidebar = useSidebarStore(); const sidebar = useSidebarStore();
const { setSidebar } = useAppStoreActions(); const { setSidebar } = useAppStoreActions();
const imageUrl = useCurrentSong()?.imageUrl; const imageUrl = useCurrentSong()?.imageUrl;
const showImage = sidebar.image; const showImage = sidebar.image;
const playlistsQuery = usePlaylistList({
limit: 0,
sortBy: PlaylistListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
});
const handleCreatePlaylistModal = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
openModal({
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
size: 'sm',
title: 'Create Playlist',
});
};
return ( return (
<SidebarContainer> <SidebarContainer>
<Stack <Stack
h="100%"
justify="space-between" justify="space-between"
spacing={0} spacing={0}
sx={{ height: '100%' }}
> >
<Stack <Stack
sx={{ spacing={0}
maxHeight: showImage ? `calc(100% - ${sidebar.leftWidth})` : '100%', sx={{ maxHeight: showImage ? `calc(100% - ${sidebar.leftWidth})` : '100%' }}
}}
> >
<ActionsContainer p={10}> <ActionsContainer p={10}>
<Grid.Col span={8}> <Grid.Col span={8}>
@ -120,121 +142,167 @@ export const Sidebar = () => {
</Group> </Group>
</Grid.Col> </Grid.Col>
</ActionsContainer> </ActionsContainer>
<Stack
spacing={0} <ScrollArea
sx={{ overflowY: 'auto' }} offsetScrollbars={false}
scrollbarSize={6}
> >
<SidebarItem to={AppRoute.HOME}> <Stack spacing={0}>
<Group> <SidebarItem
{location.pathname === AppRoute.HOME ? ( px="1rem"
<RiHome5Fill size={15} /> py="0.5rem"
) : ( to={AppRoute.HOME}
<RiHome5Line size={15} /> >
)} <Group fw="600">
Home {location.pathname === AppRoute.HOME ? (
</Group> <RiHome5Fill size={15} />
</SidebarItem> ) : (
<Accordion <RiHome5Line size={15} />
multiple )}
styles={{ Home
control: { </Group>
'&:hover': { background: 'none', color: 'var(--sidebar-fg-hover)' }, </SidebarItem>
color: 'var(--sidebar-fg)', <Accordion
transition: 'color 0.2s ease-in-out', multiple
}, styles={{
item: { borderBottom: 'none', color: 'var(--sidebar-fg)' }, control: {
itemTitle: { color: 'var(--sidebar-fg)' }, '&:hover': { background: 'none', color: 'var(--sidebar-fg-hover)' },
panel: { color: 'var(--sidebar-fg)',
marginLeft: '1rem', padding: '1rem 1rem',
}, transition: 'color 0.2s ease-in-out',
}} },
value={sidebar.expanded} item: { borderBottom: 'none', color: 'var(--sidebar-fg)' },
onChange={(e) => setSidebar({ expanded: e })} itemTitle: { color: 'var(--sidebar-fg)' },
> label: { fontWeight: 600 },
<Accordion.Item value="library"> panel: { padding: '0 1rem' },
<Accordion.Control p="1rem"> }}
<Group> value={sidebar.expanded}
{location.pathname.includes('/library/') ? ( onChange={(e) => setSidebar({ expanded: e })}
<RiDatabaseFill size={15} /> >
) : ( <Accordion.Item value="library">
<RiDatabaseLine size={15} /> <Accordion.Control>
)}
Library
</Group>
</Accordion.Control>
<Accordion.Panel>
<SidebarItem to={AppRoute.LIBRARY_ALBUMS}>
<Group> <Group>
{location.pathname === AppRoute.LIBRARY_ALBUMS ? ( {location.pathname.includes('/library/') ? (
<RiAlbumFill /> <RiDatabaseFill size={15} />
) : ( ) : (
<RiAlbumLine /> <RiDatabaseLine size={15} />
)} )}
Albums Library
</Group> </Group>
</SidebarItem> </Accordion.Control>
<SidebarItem to={AppRoute.LIBRARY_SONGS}> <Accordion.Panel>
<SidebarItem to={AppRoute.LIBRARY_ALBUMS}>
<Group>
{location.pathname === AppRoute.LIBRARY_ALBUMS ? (
<RiAlbumFill />
) : (
<RiAlbumLine />
)}
Albums
</Group>
</SidebarItem>
<SidebarItem to={AppRoute.LIBRARY_SONGS}>
<Group>
{location.pathname === AppRoute.LIBRARY_SONGS ? (
<RiMusicFill />
) : (
<RiMusicLine />
)}
Tracks
</Group>
</SidebarItem>
<SidebarItem to={AppRoute.LIBRARY_ALBUMARTISTS}>
<Group>
{location.pathname === AppRoute.LIBRARY_ALBUMARTISTS ? (
<RiUserVoiceFill />
) : (
<RiUserVoiceLine />
)}
Album Artists
</Group>
</SidebarItem>
<SidebarItem
disabled
to={AppRoute.LIBRARY_FOLDERS}
>
<Group>
<RiFlag2Line />
Genres
</Group>
</SidebarItem>
<SidebarItem
disabled
to={AppRoute.LIBRARY_FOLDERS}
>
<Group>
<RiFolder3Line />
Folders
</Group>
</SidebarItem>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="collections">
<Accordion.Control disabled>
<Group> <Group>
{location.pathname === AppRoute.LIBRARY_SONGS ? ( <BsCollection size={15} />
<RiMusicFill /> Collections
) : (
<RiMusicLine />
)}
Tracks
</Group> </Group>
</SidebarItem> </Accordion.Control>
<SidebarItem to={AppRoute.LIBRARY_ALBUMARTISTS}> <Accordion.Panel />
<Group> </Accordion.Item>
<RiUserVoiceLine /> <Accordion.Item value="playlists">
Album Artists <Accordion.Control>
<Group
noWrap
position="apart"
>
<Group noWrap>
<RiPlayListLine size={15} />
Playlists
</Group>
<Group
noWrap
spacing="xs"
>
<Button
compact
component="div"
h={13}
tooltip={{ label: 'Create playlist', openDelay: 500 }}
variant="subtle"
onClick={handleCreatePlaylistModal}
>
<RiAddFill size={13} />
</Button>
<Button
compact
component={Link}
h={13}
to={AppRoute.PLAYLISTS}
tooltip={{ label: 'Playlist list', openDelay: 500 }}
variant="subtle"
onClick={(e) => e.stopPropagation()}
>
<RiMenuUnfoldLine size={13} />
</Button>
</Group>
</Group> </Group>
</SidebarItem> </Accordion.Control>
<SidebarItem <Accordion.Panel>
disabled {playlistsQuery?.data?.items?.map((playlist) => (
to={AppRoute.LIBRARY_FOLDERS} <SidebarItem
> key={`sidebar-playlist-${playlist.id}`}
<Group> p={0}
<RiFlag2Line /> to={generatePath(AppRoute.PLAYLISTS_DETAIL, { playlistId: playlist.id })}
Genres >
</Group> {playlist.name}
</SidebarItem> </SidebarItem>
<SidebarItem ))}
disabled </Accordion.Panel>
to={AppRoute.LIBRARY_FOLDERS} </Accordion.Item>
> </Accordion>
<Group> </Stack>
<RiFolder3Line /> </ScrollArea>
Folders
</Group>
</SidebarItem>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="collections">
<Accordion.Control
disabled
p="1rem"
>
<Group>
<BsCollection size={15} />
Collections
</Group>
</Accordion.Control>
<Accordion.Panel />
</Accordion.Item>
<Accordion.Item value="playlists">
<Accordion.Control
disabled
p="1rem"
>
<Group>
<RiPlayListLine size={15} />
Playlists
</Group>
</Accordion.Control>
<Accordion.Panel />
</Accordion.Item>
</Accordion>
</Stack>
</Stack> </Stack>
<AnimatePresence <AnimatePresence
initial={false} initial={false}

View file

@ -1,4 +1,4 @@
import type { UseQueryOptions, DefaultOptions } from '@tanstack/react-query'; import type { UseQueryOptions, DefaultOptions, UseMutationOptions } from '@tanstack/react-query';
import { QueryClient, QueryCache } from '@tanstack/react-query'; import { QueryClient, QueryCache } from '@tanstack/react-query';
import { toast } from '/@/renderer/components'; import { toast } from '/@/renderer/components';
@ -49,3 +49,13 @@ export type QueryOptions = {
suspense?: UseQueryOptions['suspense']; suspense?: UseQueryOptions['suspense'];
useErrorBoundary?: boolean; useErrorBoundary?: boolean;
}; };
export type MutationOptions = {
mutationKey: UseMutationOptions['mutationKey'];
onError?: (err: any) => void;
onSettled?: any;
onSuccess?: any;
retry?: UseQueryOptions['retry'];
retryDelay?: UseQueryOptions['retryDelay'];
useErrorBoundary?: boolean;
};