Update react-query implementation

This commit is contained in:
jeffvli
2022-11-16 19:52:25 -08:00
parent 82d4ad5502
commit 3827953b59
33 changed files with 338 additions and 362 deletions
+2 -2
View File
@@ -1,3 +1,3 @@
export * from './queries/use-album-detail'; export * from './queries/get-album-detail';
export * from './queries/use-album-list'; export * from './queries/get-album-list';
export * from './routes/album-list-route'; export * from './routes/album-list-route';
@@ -3,11 +3,10 @@ import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys'; import { queryKeys } from '@/renderer/api/query-keys';
import { QueryOptions } from '@/renderer/lib/react-query'; import { QueryOptions } from '@/renderer/lib/react-query';
import { useAuthStore } from '@/renderer/store'; import { useAuthStore } from '@/renderer/store';
import { AlbumDetailResponse } from 'renderer/api/types';
export const useAlbumDetail = ( export const useAlbumDetail = (
query: { albumId: string }, query: { albumId: string },
options: QueryOptions<AlbumDetailResponse> options: QueryOptions
) => { ) => {
const serverId = useAuthStore((state) => state.currentServer?.id) || ''; const serverId = useAuthStore((state) => state.currentServer?.id) || '';
@@ -2,20 +2,28 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { api } from '@/renderer/api'; import { api } from '@/renderer/api';
import { AlbumListParams } from '@/renderer/api/albums.api'; import { AlbumListParams } from '@/renderer/api/albums.api';
import { queryKeys } from '@/renderer/api/query-keys'; import { queryKeys } from '@/renderer/api/query-keys';
import { QueryOptions } from '@/renderer/lib/react-query';
import { useAuthStore } from '@/renderer/store'; import { useAuthStore } from '@/renderer/store';
import { AlbumListResponse } from 'renderer/api/types'; import { AlbumListResponse } from 'renderer/api/types';
export const useAlbumList = (params: AlbumListParams) => { export const useAlbumList = (
params: AlbumListParams,
options?: QueryOptions
) => {
const serverId = useAuthStore((state) => state.currentServer?.id) || ''; const serverId = useAuthStore((state) => state.currentServer?.id) || '';
return useQuery({ return useQuery({
enabled: !!serverId, enabled: !!serverId,
queryFn: () => api.albums.getAlbumList({ serverId }, params), queryFn: () => api.albums.getAlbumList({ serverId }, params),
queryKey: queryKeys.albums.list(serverId, params), queryKey: queryKeys.albums.list(serverId, params),
...options,
}); });
}; };
export const useAlbumListInfinite = (params: AlbumListParams) => { export const useAlbumListInfinite = (
params: AlbumListParams,
options?: QueryOptions
) => {
const serverId = useAuthStore((state) => state.currentServer?.id) || ''; const serverId = useAuthStore((state) => state.currentServer?.id) || '';
return useInfiniteQuery({ return useInfiniteQuery({
@@ -29,5 +37,6 @@ export const useAlbumListInfinite = (params: AlbumListParams) => {
queryFn: ({ pageParam }) => queryFn: ({ pageParam }) =>
api.albums.getAlbumList({ serverId }, { ...(pageParam || params) }), api.albums.getAlbumList({ serverId }, { ...(pageParam || params) }),
queryKey: queryKeys.albums.list(serverId, params), queryKey: queryKeys.albums.list(serverId, params),
...options,
}); });
}; };
@@ -34,7 +34,7 @@ import {
FilterGroupType, FilterGroupType,
encodeAdvancedFiltersQuery, encodeAdvancedFiltersQuery,
} from '@/renderer/features/albums/components/advanced-filters'; } from '@/renderer/features/albums/components/advanced-filters';
import { useAlbumList } from '@/renderer/features/albums/queries/use-album-list'; import { useAlbumList } from '@/renderer/features/albums/queries/get-album-list';
import { useServerList } from '@/renderer/features/servers'; import { useServerList } from '@/renderer/features/servers';
import { AnimatedPage, useServerCredential } from '@/renderer/features/shared'; import { AnimatedPage, useServerCredential } from '@/renderer/features/shared';
import { AppRoute } from '@/renderer/router/routes'; import { AppRoute } from '@/renderer/router/routes';
@@ -80,7 +80,7 @@ export const AlbumListRoute = () => {
const page = useAppStore((state) => state.albums); const page = useAppStore((state) => state.albums);
const setPage = useAppStore((state) => state.setPage); const setPage = useAppStore((state) => state.setPage);
const serverId = useAuthStore((state) => state.currentServer?.id) || ''; const serverId = useAuthStore((state) => state.currentServer?.id) || '';
const { data: servers } = useServerList({ enabled: true }); const serverListQuery = useServerList({ enabled: true });
const [filters, setFilters] = useSetState({ const [filters, setFilters] = useSetState({
orderBy: SortOrder.ASC, orderBy: SortOrder.ASC,
serverFolderId: [] as string[], serverFolderId: [] as string[],
@@ -107,11 +107,13 @@ export const AlbumListRoute = () => {
}; };
const serverFolders = useMemo(() => { const serverFolders = useMemo(() => {
const server = servers?.data.find((server) => server.id === serverId); const server = serverListQuery?.data?.data.find(
(server) => server.id === serverId
);
return server?.serverFolders; return server?.serverFolders;
}, [serverId, servers]); }, [serverId, serverListQuery?.data?.data]);
const { data: albums } = useAlbumList({ const albumListQuery = useAlbumList({
advancedFilters, advancedFilters,
orderBy: filters.orderBy, orderBy: filters.orderBy,
serverFolderId: filters.serverFolderId, serverFolderId: filters.serverFolderId,
@@ -454,7 +456,7 @@ export const AlbumListRoute = () => {
display={page.list?.display || CardDisplayType.CARD} display={page.list?.display || CardDisplayType.CARD}
fetchFn={fetch} fetchFn={fetch}
height={height} height={height}
itemCount={albums?.pagination.totalEntries || 0} itemCount={albumListQuery?.data?.pagination.totalEntries || 0}
itemGap={20} itemGap={20}
itemSize={150 + page.list?.size} itemSize={150 + page.list?.size}
itemType={LibraryItem.ALBUM} itemType={LibraryItem.ALBUM}
+1 -1
View File
@@ -1 +1 @@
export * from './queries/genre-list'; export * from './queries/get-genre-list';
@@ -1,20 +1,17 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { api } from '@/renderer/api'; import { api } from '@/renderer/api';
import { GenreListResponse } from '@/renderer/api/genres.api';
import { queryKeys } from '@/renderer/api/query-keys'; import { queryKeys } from '@/renderer/api/query-keys';
import { ApiError } from '@/renderer/api/types';
import { QueryOptions } from '@/renderer/lib/react-query'; import { QueryOptions } from '@/renderer/lib/react-query';
import { useAuthStore } from '@/renderer/store'; import { useAuthStore } from '@/renderer/store';
export const useGenreList = (options?: QueryOptions<GenreListResponse>) => { export const useGenreList = (options?: QueryOptions) => {
const serverId = useAuthStore((state) => state.currentServer?.id) || ''; const serverId = useAuthStore((state) => state.currentServer?.id) || '';
const query = useQuery<GenreListResponse, AxiosError<ApiError>>({ const query = useQuery({
enabled: !!serverId, enabled: !!serverId,
queryFn: ({ signal }) => api.genres.getGenreList({ serverId }, signal), queryFn: ({ signal }) => api.genres.getGenreList({ serverId }, signal),
queryKey: queryKeys.genres.list(serverId), queryKey: queryKeys.genres.list(serverId),
staleTime: Infinity, staleTime: 1000 * 60 * 60,
...options, ...options,
}); });
@@ -9,7 +9,7 @@ import {
TextInput, TextInput,
SegmentedControl, SegmentedControl,
} from '@/renderer/components'; } from '@/renderer/components';
import { useCreateServer } from '@/renderer/features/servers/mutations/use-create-server'; import { useCreateServer } from '@/renderer/features/servers/mutations/create-server';
const SERVER_TYPES = [ const SERVER_TYPES = [
{ label: 'Jellyfin', value: ServerType.JELLYFIN }, { label: 'Jellyfin', value: ServerType.JELLYFIN },
@@ -3,7 +3,7 @@ import { useForm, zodResolver } from '@mantine/form';
import { useFocusTrap } from '@mantine/hooks'; import { useFocusTrap } from '@mantine/hooks';
import { z } from 'zod'; import { z } from 'zod';
import { Button, TextInput } from '@/renderer/components'; import { Button, TextInput } from '@/renderer/components';
import { useCreateServerUrl } from '@/renderer/features/servers/mutations/use-create-server-url'; import { useCreateServerUrl } from '@/renderer/features/servers/mutations/create-server-url';
interface AddServerUrlFormProps { interface AddServerUrlFormProps {
onCancel: () => void; onCancel: () => void;
@@ -3,7 +3,7 @@ import { useForm } from '@mantine/form';
import { RiInformationLine } from 'react-icons/ri'; import { RiInformationLine } from 'react-icons/ri';
import { Server, ServerType } from '@/renderer/api/types'; import { Server, ServerType } from '@/renderer/api/types';
import { Button, PasswordInput, TextInput, toast } from '@/renderer/components'; import { Button, PasswordInput, TextInput, toast } from '@/renderer/components';
import { useUpdateServer } from '@/renderer/features/servers/mutations/use-update-server'; import { useUpdateServer } from '@/renderer/features/servers/mutations/update-server';
interface EditServerFormProps { interface EditServerFormProps {
onCancel: () => void; onCancel: () => void;
@@ -22,18 +22,17 @@ import { AddServerCredentialForm } from '@/renderer/features/servers/components/
import { AddServerUrlForm } from '@/renderer/features/servers/components/add-server-url-form'; import { AddServerUrlForm } from '@/renderer/features/servers/components/add-server-url-form';
import { EditServerForm } from '@/renderer/features/servers/components/edit-server-form'; import { EditServerForm } from '@/renderer/features/servers/components/edit-server-form';
import { ServerSection } from '@/renderer/features/servers/components/server-section'; import { ServerSection } from '@/renderer/features/servers/components/server-section';
import { useDeleteServerUrl } from '@/renderer/features/servers/mutations/use-delete-server-url'; import { useDeleteServerUrl } from '@/renderer/features/servers/mutations/delete-server-url';
import { useDisableServerFolder } from '@/renderer/features/servers/mutations/use-disable-server-folder'; import { useDisableServerFolder } from '@/renderer/features/servers/mutations/disable-server-folder';
import { useDisableServerUrl } from '@/renderer/features/servers/mutations/use-disable-server-url'; import { useDisableServerUrl } from '@/renderer/features/servers/mutations/disable-server-url';
import { useEnableServerFolder } from '@/renderer/features/servers/mutations/use-enable-server-folder'; import { useEnableServerFolder } from '@/renderer/features/servers/mutations/enable-server-folder';
import { useEnableServerUrl } from '@/renderer/features/servers/mutations/use-enable-server-url'; import { useEnableServerUrl } from '@/renderer/features/servers/mutations/enable-server-url';
import { useFullScan } from '@/renderer/features/servers/mutations/use-full-scan'; import { useFullScan } from '@/renderer/features/servers/mutations/start-full-scan';
import { useQuickScan } from '@/renderer/features/servers/mutations/use-quick-scan'; import { useQuickScan } from '@/renderer/features/servers/mutations/start-quick-scan';
import { useUpdateServer } from '@/renderer/features/servers/mutations/use-update-server'; import { useUpdateServer } from '@/renderer/features/servers/mutations/update-server';
import { ServerPermission, usePermissions } from '@/renderer/features/shared'; import { ServerPermission, usePermissions } from '@/renderer/features/shared';
import { useTaskList } from '@/renderer/features/tasks'; import { useTaskList } from '@/renderer/features/tasks';
import { useAuthStore } from '@/renderer/store'; import { useAuthStore } from '@/renderer/store';
import { Font } from '@/renderer/styles';
interface ServerListItemProps { interface ServerListItemProps {
server: Server; server: Server;
@@ -169,100 +168,221 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
}; };
return ( return (
<> <Stack mt="1rem" p="1rem" spacing="xl">
<Stack mt="1rem" spacing="xl"> <ServerSection
<ServerSection title={
title={ <Group position="apart">
<Group position="apart"> <Text>Server details</Text>
<Text font={Font.EPILOGUE}>Server details</Text> <Group spacing="md">
<Group spacing="md">
{serverPermission >= ServerPermission.ADMIN && (
<Button
compact
disabled={isRunningTask}
loading={fullScan.isLoading}
variant="subtle"
onClick={handleFullScan}
>
Full scan
</Button>
)}
{serverPermission >= ServerPermission.EDITOR && (
<Button
compact
disabled={true || isRunningTask}
variant="subtle"
onClick={handleQuickScan}
>
Quick scan
</Button>
)}
</Group>
</Group>
}
>
{edit ? (
<EditServerForm
server={server}
onCancel={() => editHandlers.toggle()}
/>
) : (
<Group position="apart">
<Group>
<Stack>
<Text>URL</Text>
{serverPermission >= ServerPermission.EDITOR && (
<Text>Username</Text>
)}
</Stack>
<Stack>
<Text size="sm">{server.url}</Text>
{serverPermission >= ServerPermission.EDITOR && (
<Text size="sm">{server.username}</Text>
)}
</Stack>
</Group>
{serverPermission >= ServerPermission.ADMIN && ( {serverPermission >= ServerPermission.ADMIN && (
<Group> <Button
<Button compact
tooltip={{ label: 'Edit server details' }} disabled={isRunningTask}
variant="subtle" loading={fullScan.isLoading}
onClick={() => editHandlers.toggle()} variant="subtle"
> onClick={handleFullScan}
<RiEdit2Fill /> >
</Button> Full scan
</Group> </Button>
)}
{serverPermission >= ServerPermission.EDITOR && (
<Button
compact
disabled={true || isRunningTask}
variant="subtle"
onClick={handleQuickScan}
>
Quick scan
</Button>
)} )}
</Group> </Group>
)} </Group>
</ServerSection> }
>
<ServerSection {edit ? (
title={ <EditServerForm
server={server}
onCancel={() => editHandlers.toggle()}
/>
) : (
<Group position="apart">
<Group> <Group>
<Text>Music Folders</Text> <Stack>
<Tooltip label="Select which music folders you want to enable. Enabled music folders are included in the scan queue and are available to browse."> <Text>URL</Text>
<Group> {serverPermission >= ServerPermission.EDITOR && (
<RiInformationLine /> <Text>Username</Text>
</Group> )}
</Tooltip> </Stack>
<Stack>
<Text size="sm">{server.url}</Text>
{serverPermission >= ServerPermission.EDITOR && (
<Text size="sm">{server.username}</Text>
)}
</Stack>
</Group> </Group>
} {serverPermission >= ServerPermission.ADMIN && (
> <Group>
<Stack> <Button
{server.serverFolders?.map((folder) => ( tooltip={{ label: 'Edit server details' }}
<Group key={folder.id} position="apart"> variant="subtle"
<Text size="sm">{folder.name}</Text> onClick={() => editHandlers.toggle()}
<Group> >
<> <RiEdit2Fill />
</Button>
</Group>
)}
</Group>
)}
</ServerSection>
<ServerSection
title={
<Group>
<Text>Music Folders</Text>
<Tooltip label="Select which music folders you want to enable. Enabled music folders are included in the scan queue and are available to browse.">
<Group>
<RiInformationLine />
</Group>
</Tooltip>
</Group>
}
>
<Stack>
{server.serverFolders?.map((folder) => (
<Group key={folder.id} position="apart">
<Text size="sm">{folder.name}</Text>
<Group>
<>
<Switch
checked={folder.enabled}
disabled={serverPermission < ServerPermission.ADMIN}
onChange={(e) =>
handleToggleFolder(folder.id, !e.currentTarget.checked)
}
/>
{serverPermission >= ServerPermission.ADMIN && (
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button compact variant="subtle">
<RiMore2Fill size={15} />
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
disabled
rightSection={
<RiDeleteBin2Line color="var(--danger-color)" />
}
>
Delete
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
)}
</>
</Group>
</Group>
))}
</Stack>
</ServerSection>
<ServerSection
title={
<Group>
<Text>Credentials</Text>
<Tooltip label="If the server admin has required user credentials for this server, you will need to add your own login details here. Any credentials entered here are stored LOCALLY in the browser.">
<Group>
<RiInformationLine />
</Group>
</Tooltip>
</Group>
}
>
{addCredential ? (
<AddServerCredentialForm
server={server}
onCancel={() => addCredentialHandlers.close()}
/>
) : (
<>
<Stack>
{serverCredentials?.map((credential) => (
<Group key={credential.id} position="apart">
<Text size="sm">{credential.username}</Text>
<Group>
<Switch <Switch
checked={folder.enabled} checked={credential.enabled}
disabled={serverPermission < ServerPermission.ADMIN}
onChange={(e) => onChange={(e) =>
handleToggleFolder(folder.id, !e.currentTarget.checked) handleToggleCredential(
credential.id,
!e.currentTarget.checked
)
} }
/> />
{serverPermission >= ServerPermission.ADMIN && ( <DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button compact variant="subtle">
<RiMore2Fill size={15} />
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
rightSection={
<RiDeleteBin2Line color="var(--danger-color)" />
}
onClick={() => handleDeleteCredential(credential.id)}
>
Delete
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
</Group>
</Group>
))}
</Stack>
<Button
compact
mt={10}
variant="subtle"
onClick={() => addCredentialHandlers.open()}
>
Add credential
</Button>
</>
)}
</ServerSection>
<ServerSection
title={
<Group>
<Text>URLs</Text>
<Tooltip label="Enabling a URL will use it to generate image and audio endpoints instead of the server-defined URL. This is useful if you need to be able to access a URL from a local and remote network (e.g. http://localhost:4533 or https://music.domain.net)">
<Group>
<RiInformationLine />
</Group>
</Tooltip>
</Group>
}
>
{addUrl ? (
<AddServerUrlForm
serverId={server.id}
onCancel={() => addUrlHandlers.close()}
/>
) : (
<>
<Stack>
{server.serverUrls?.map((serverUrl) => (
<Group key={serverUrl.id} position="apart">
<Text size="sm">{serverUrl.url}</Text>
<Group>
<Switch
checked={serverUrl.enabled}
onChange={(e) =>
handleToggleUrl(serverUrl.id, !e.currentTarget.checked)
}
/>
{serverPermission >= ServerPermission.EDITOR && (
<DropdownMenu position="bottom-start"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
<Button compact variant="subtle"> <Button compact variant="subtle">
@@ -271,201 +391,73 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
</DropdownMenu.Target> </DropdownMenu.Target>
<DropdownMenu.Dropdown> <DropdownMenu.Dropdown>
<DropdownMenu.Item <DropdownMenu.Item
disabled
rightSection={ rightSection={
<RiDeleteBin2Line color="var(--danger-color)" /> <RiDeleteBin2Line color="var(--danger-color)" />
} }
onClick={() => handleDeleteUrl(serverUrl.id)}
> >
Delete Delete
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
)} )}
</>
</Group>
</Group>
))}
</Stack>
</ServerSection>
<ServerSection
title={
<Group>
<Text>Credentials</Text>
<Tooltip label="If the server admin has required user credentials for this server, you will need to add your own login details here. Any credentials entered here are stored LOCALLY in the browser.">
<Group>
<RiInformationLine />
</Group>
</Tooltip>
</Group>
}
>
{addCredential ? (
<AddServerCredentialForm
server={server}
onCancel={() => addCredentialHandlers.close()}
/>
) : (
<>
<Stack>
{serverCredentials?.map((credential) => (
<Group key={credential.id} position="apart">
<Text size="sm">{credential.username}</Text>
<Group>
<Switch
checked={credential.enabled}
onChange={(e) =>
handleToggleCredential(
credential.id,
!e.currentTarget.checked
)
}
/>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button compact variant="subtle">
<RiMore2Fill size={15} />
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
rightSection={
<RiDeleteBin2Line color="var(--danger-color)" />
}
onClick={() =>
handleDeleteCredential(credential.id)
}
>
Delete
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
</Group>
</Group> </Group>
))} </Group>
</Stack> ))}
</Stack>
{serverPermission >= ServerPermission.EDITOR && (
<Button <Button
compact compact
mt={10} mt={10}
variant="subtle" variant="subtle"
onClick={() => addCredentialHandlers.open()} onClick={() => addUrlHandlers.open()}
> >
Add credential Add URL
</Button> </Button>
</> )}
)} </>
</ServerSection> )}
</ServerSection>
<ServerSection {serverPermission >= ServerPermission.ADMIN && (
title={ <ServerSection title="Danger zone">
<Group position="apart">
<Group> <Group>
<Text>URLs</Text> <Text size="sm">Require user credentials</Text>
<Tooltip label="Enabling a URL will use it to generate image and audio endpoints instead of the server-defined URL. This is useful if you need to be able to access a URL from a local and remote network (e.g. http://localhost:4533 or https://music.domain.net)"> <Tooltip label="WARNING: Disabling this option will expose your server login credentials to all users. If enabled, all users will be required to enter their own credentials to access this server.">
<Group> <Group>
<RiInformationLine /> <RiInformationLine />
</Group> </Group>
</Tooltip> </Tooltip>
</Group> </Group>
} <Switch
> checked={server.noCredential}
{addUrl ? ( onChange={(e) =>
<AddServerUrlForm toggleRequiredCredential(e.currentTarget.checked)
serverId={server.id} }
onCancel={() => addUrlHandlers.close()}
/> />
) : ( </Group>
{permissions.isSuperAdmin && (
<> <>
<Stack> <Divider my="xl" />
{server.serverUrls?.map((serverUrl) => ( <Button
<Group key={serverUrl.id} position="apart"> compact
<Text size="sm">{serverUrl.url}</Text> disabled
<Group> leftIcon={<RiDeleteBin2Line />}
<Switch size="xs"
checked={serverUrl.enabled} sx={{
onChange={(e) => '&:hover': {
handleToggleUrl( background: 'var(--danger-color)',
serverUrl.id, },
!e.currentTarget.checked background: 'var(--danger-color)',
) }}
} >
/> Delete server
{serverPermission >= ServerPermission.EDITOR && ( </Button>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button compact variant="subtle">
<RiMore2Fill size={15} />
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
rightSection={
<RiDeleteBin2Line color="var(--danger-color)" />
}
onClick={() => handleDeleteUrl(serverUrl.id)}
>
Delete
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
)}
</Group>
</Group>
))}
</Stack>
{serverPermission >= ServerPermission.EDITOR && (
<Button
compact
mt={10}
variant="subtle"
onClick={() => addUrlHandlers.open()}
>
Add URL
</Button>
)}
</> </>
)} )}
</ServerSection> </ServerSection>
)}
{serverPermission >= ServerPermission.ADMIN && ( </Stack>
<ServerSection title="Danger zone">
<Group position="apart">
<Group>
<Text size="sm">Require user credentials</Text>
<Tooltip label="WARNING: Disabling this option will expose your server login credentials to all users. If enabled, all users will be required to enter their own credentials to access this server.">
<Group>
<RiInformationLine />
</Group>
</Tooltip>
</Group>
<Switch
checked={server.noCredential}
onChange={(e) =>
toggleRequiredCredential(e.currentTarget.checked)
}
/>
</Group>
{permissions.isSuperAdmin && (
<>
<Divider my="xl" />
<Button
compact
disabled
leftIcon={<RiDeleteBin2Line />}
size="xs"
sx={{
'&:hover': {
background: 'var(--danger-color)',
},
background: 'var(--danger-color)',
}}
>
Delete server
</Button>
</>
)}
</ServerSection>
)}
</Stack>
</>
); );
}; };
@@ -3,13 +3,13 @@ import { openContextModal } from '@mantine/modals';
import { RiAddFill, RiServerFill } from 'react-icons/ri'; import { RiAddFill, RiServerFill } from 'react-icons/ri';
import { Button, ContextModalVars, Accordion } from '@/renderer/components'; import { Button, ContextModalVars, Accordion } from '@/renderer/components';
import { ServerListItem } from '@/renderer/features/servers/components/server-list-item'; import { ServerListItem } from '@/renderer/features/servers/components/server-list-item';
import { useServerList } from '@/renderer/features/servers/queries/use-server-list'; import { useServerList } from '@/renderer/features/servers/queries/get-server-list';
import { usePermissions } from '@/renderer/features/shared'; import { usePermissions } from '@/renderer/features/shared';
import { titleCase } from '@/renderer/utils'; import { titleCase } from '@/renderer/utils';
import { AddServerForm } from './add-server-form'; import { AddServerForm } from './add-server-form';
export const ServerList = () => { export const ServerList = () => {
const { data: servers } = useServerList(); const serverListQuery = useServerList();
const permissions = usePermissions(); const permissions = usePermissions();
const handleAddServerModal = () => { const handleAddServerModal = () => {
@@ -49,7 +49,7 @@ export const ServerList = () => {
)} )}
</Group> </Group>
<Accordion variant="separated"> <Accordion variant="separated">
{servers?.data?.map((s) => ( {serverListQuery?.data?.data.map((s) => (
<Accordion.Item key={s.id} value={s.name}> <Accordion.Item key={s.id} value={s.name}>
<Accordion.Control icon={<RiServerFill size={15} />}> <Accordion.Control icon={<RiServerFill size={15} />}>
<Group position="apart"> <Group position="apart">
+2 -2
View File
@@ -1,7 +1,7 @@
export * from './mutations/use-create-server'; export * from './mutations/create-server';
export * from './components/add-server-form'; export * from './components/add-server-form';
export * from './components/server-list'; export * from './components/server-list';
export * from './queries/use-server-list'; export * from './queries/get-server-list';
export * from './queries/get-server-map'; export * from './queries/get-server-map';
export * from './mutations/create-server-folder-permission'; export * from './mutations/create-server-folder-permission';
export * from './mutations/delete-server-folder-permission'; export * from './mutations/delete-server-folder-permission';
@@ -7,14 +7,14 @@ import { useAuthStore } from '@/renderer/store';
export const useServerList = ( export const useServerList = (
params?: { enabled?: boolean }, params?: { enabled?: boolean },
options?: QueryOptions<ServerListResponse> options?: QueryOptions
) => { ) => {
const currentServer = useAuthStore((state) => state.currentServer); const currentServer = useAuthStore((state) => state.currentServer);
const setCurrentServer = useAuthStore((state) => state.setCurrentServer); const setCurrentServer = useAuthStore((state) => state.setCurrentServer);
return useQuery({ return useQuery({
onSuccess: (data) => { onSettled: (data: ServerListResponse) => {
const currentServerFromList = data.data.find( const currentServerFromList = data?.data.find(
(server) => server.id === currentServer?.id (server) => server.id === currentServer?.id
); );
@@ -1,11 +1,10 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api } from '@/renderer/api'; import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys'; import { queryKeys } from '@/renderer/api/query-keys';
import { ServerMapResponse } from '@/renderer/api/servers.api';
import { QueryOptions } from '@/renderer/lib/react-query'; import { QueryOptions } from '@/renderer/lib/react-query';
export const useServerMap = (options?: QueryOptions<ServerMapResponse>) => { export const useServerMap = (options?: QueryOptions) => {
return useQuery<ServerMapResponse>({ return useQuery({
cacheTime: Infinity, cacheTime: Infinity,
queryFn: ({ signal }) => api.servers.getServerMap(signal), queryFn: ({ signal }) => api.servers.getServerMap(signal),
queryKey: queryKeys.servers.map(), queryKey: queryKeys.servers.map(),
+1 -1
View File
@@ -1,3 +1,3 @@
export * from './queries/task-list'; export * from './queries/get-task-list';
export * from './mutations/cancel-all-tasks'; export * from './mutations/cancel-all-tasks';
export * from './mutations/cancel-task'; export * from './mutations/cancel-task';
@@ -5,7 +5,7 @@ import { queryKeys } from '@/renderer/api/query-keys';
import { TaskListResponse } from '@/renderer/api/tasks.api'; import { TaskListResponse } from '@/renderer/api/tasks.api';
import { QueryOptions } from '@/renderer/lib/react-query'; import { QueryOptions } from '@/renderer/lib/react-query';
export const useTaskList = (options?: QueryOptions<TaskListResponse>) => { export const useTaskList = (options?: QueryOptions) => {
return useQuery({ return useQuery({
queryFn: ({ signal }) => api.tasks.getActiveTasks(signal), queryFn: ({ signal }) => api.tasks.getActiveTasks(signal),
queryKey: queryKeys.tasks.list(), queryKey: queryKeys.tasks.list(),
@@ -21,9 +21,9 @@ export const ActivityMenu = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const serverId = useAuthStore((state) => state.currentServer?.id) || ''; const serverId = useAuthStore((state) => state.currentServer?.id) || '';
const [isTaskRunning, setIsTaskRunning] = useState(false); const [isTaskRunning, setIsTaskRunning] = useState(false);
const { data: tasks, refetch } = useTaskList({ const taskListQuery = useTaskList({
onSuccess: (data) => { onSuccess: (data: ReturnType<typeof useTaskList>['data']) => {
if (data.data.length === 0) { if (data?.data.length === 0) {
queryClient.invalidateQueries(queryKeys.server.root(serverId)); queryClient.invalidateQueries(queryKeys.server.root(serverId));
return setIsTaskRunning(false); return setIsTaskRunning(false);
} }
@@ -57,14 +57,14 @@ export const ActivityMenu = () => {
useEffect(() => { useEffect(() => {
socket.on('task:started', () => { socket.on('task:started', () => {
setTimeout(() => refetch(), 1000); setTimeout(() => taskListQuery.refetch(), 1000);
setIsTaskRunning(true); setIsTaskRunning(true);
}); });
return () => { return () => {
socket.off('task:started'); socket.off('task:started');
}; };
}, [refetch]); }, [taskListQuery]);
return ( return (
<> <>
@@ -80,7 +80,7 @@ export const ActivityMenu = () => {
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
{isTaskRunning ? ( {isTaskRunning ? (
tasks?.data?.map((task) => ( taskListQuery.data?.data?.map((task) => (
<Group key={task.id} position="apart"> <Group key={task.id} position="apart">
<Text>{task.note}</Text> <Text>{task.note}</Text>
</Group> </Group>
@@ -40,8 +40,8 @@ export const EditUserPermissionsForm = ({
onCancel, onCancel,
}: EditUserPermissionsFormProps) => { }: EditUserPermissionsFormProps) => {
const permissions = usePermissions(); const permissions = usePermissions();
const { data: servers } = useServerList(); const serverListQuery = useServerList();
const { data: user } = useUserDetail({ userId }); const userDetailQuery = useUserDetail({ userId });
const createServerPermissionMutation = useCreateServerPermission(); const createServerPermissionMutation = useCreateServerPermission();
const deleteServerPermissionMutation = useDeleteServerPermission(); const deleteServerPermissionMutation = useDeleteServerPermission();
const updateServerPermissionMutation = useUpdateServerPermission(); const updateServerPermissionMutation = useUpdateServerPermission();
@@ -61,17 +61,20 @@ export const EditUserPermissionsForm = ({
}, },
]; ];
const userDetailId = userDetailQuery?.data?.data.id;
return ( return (
<Stack m={5}> <Stack m={5}>
<Accordion variant="contained"> <Accordion variant="contained">
{servers?.data?.map((s) => { {serverListQuery?.data?.data.map((s) => {
const currentServerPermission = user?.data?.serverPermissions?.find( const currentServerPermission =
(p) => p.serverId === s.id userDetailQuery?.data?.data.serverPermissions?.find(
); (p) => p.serverId === s.id
);
const isServerAdminEditingSelf = const isServerAdminEditingSelf =
permissions[s.id] >= ServerPermission.ADMIN && permissions[s.id] >= ServerPermission.ADMIN &&
user?.data.id === permissions.userId; userDetailQuery?.data?.data.id === permissions.userId;
const isServerAdminEditingOtherAdmin = const isServerAdminEditingOtherAdmin =
!permissions.isAdmin && !permissions.isAdmin &&
@@ -81,7 +84,7 @@ export const EditUserPermissionsForm = ({
isServerAdminEditingSelf || isServerAdminEditingOtherAdmin; isServerAdminEditingSelf || isServerAdminEditingOtherAdmin;
const handleChangeServerPermission = async (e: string | null) => { const handleChangeServerPermission = async (e: string | null) => {
if (!e || !user) return; if (!e || !userDetailId) return;
if (e === 'none' && currentServerPermission) { if (e === 'none' && currentServerPermission) {
deleteServerPermissionMutation.mutate( deleteServerPermissionMutation.mutate(
@@ -90,7 +93,7 @@ export const EditUserPermissionsForm = ({
permissionId: currentServerPermission.id, permissionId: currentServerPermission.id,
serverId: s.id, serverId: s.id,
}, },
userId: user.data.id, userId: userDetailId,
}, },
{ {
onError: (err) => onError: (err) =>
@@ -110,7 +113,7 @@ export const EditUserPermissionsForm = ({
permissionId: currentServerPermission.id, permissionId: currentServerPermission.id,
serverId: s.id, serverId: s.id,
}, },
userId: user.data.id, userId: userDetailId,
}, },
{ {
onError: (err) => onError: (err) =>
@@ -125,7 +128,7 @@ export const EditUserPermissionsForm = ({
{ {
body: { body: {
type: e as ServerPermissionType, type: e as ServerPermissionType,
userId: user.data.id, userId: userDetailId,
}, },
query: { query: {
serverId: s.id, serverId: s.id,
@@ -191,14 +194,14 @@ export const EditUserPermissionsForm = ({
</Stack> </Stack>
{s.serverFolders?.map((f) => { {s.serverFolders?.map((f) => {
const currentFolderPermission = const currentFolderPermission =
user?.data.serverFolderPermissions?.find( userDetailQuery?.data?.data.serverFolderPermissions?.find(
(p) => p.serverFolderId === f.id (p) => p.serverFolderId === f.id
); );
const handleToggleMusicFolderPermission = async ( const handleToggleMusicFolderPermission = async (
e: ChangeEvent<HTMLInputElement> e: ChangeEvent<HTMLInputElement>
) => { ) => {
if (!user) return; if (!userDetailId) return;
const { checked } = e.target; const { checked } = e.target;
const serverId = s.id; const serverId = s.id;
@@ -206,7 +209,7 @@ export const EditUserPermissionsForm = ({
createServerFolderPermissionMutation.mutate( createServerFolderPermissionMutation.mutate(
{ {
body: { body: {
userId: user.data.id, userId: userDetailId,
}, },
query: { query: {
folderId: f.id, folderId: f.id,
@@ -229,7 +232,7 @@ export const EditUserPermissionsForm = ({
folderPermissionId: currentFolderPermission.id, folderPermissionId: currentFolderPermission.id,
serverId, serverId,
}, },
userId: user.data.id, userId: userDetailId,
}, },
{ {
onError: (err) => onError: (err) =>
@@ -245,7 +248,7 @@ export const EditUserPermissionsForm = ({
return ( return (
<Switch <Switch
key={`server-folder-permission-${f.id}`} key={`server-folder-permission-${f.id}`}
defaultChecked={user?.data.serverFolderPermissions.some( defaultChecked={userDetailQuery?.data?.data.serverFolderPermissions.some(
(p) => p.serverFolderId === f.id (p) => p.serverFolderId === f.id
)} )}
disabled={isServerAdminEditingOtherAdmin} disabled={isServerAdminEditingOtherAdmin}
@@ -1,12 +1,11 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api } from '@/renderer/api'; import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys'; import { queryKeys } from '@/renderer/api/query-keys';
import { UserDetailResponse } from '@/renderer/api/users.api';
import { QueryOptions } from '@/renderer/lib/react-query'; import { QueryOptions } from '@/renderer/lib/react-query';
export const useUserDetail = ( export const useUserDetail = (
q: { userId: string }, q: { userId: string },
options?: QueryOptions<UserDetailResponse> options?: QueryOptions
) => { ) => {
const query = useQuery({ const query = useQuery({
queryFn: () => api.users.getUserDetail({ userId: q.userId }), queryFn: () => api.users.getUserDetail({ userId: q.userId }),
@@ -1,12 +1,11 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api } from '@/renderer/api'; import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys'; import { queryKeys } from '@/renderer/api/query-keys';
import { UserListResponse } from '@/renderer/api/users.api';
import { QueryOptions } from '@/renderer/lib/react-query'; import { QueryOptions } from '@/renderer/lib/react-query';
import { useAuthStore } from '@/renderer/store'; import { useAuthStore } from '@/renderer/store';
import { getFileUrl } from '@/renderer/utils'; import { getFileUrl } from '@/renderer/utils';
export const useUserList = (options?: QueryOptions<UserListResponse>) => { export const useUserList = (options?: QueryOptions) => {
const serverUrl = useAuthStore((state) => state.serverUrl); const serverUrl = useAuthStore((state) => state.serverUrl);
const query = useQuery({ const query = useQuery({
+2 -1
View File
@@ -4,6 +4,7 @@ import isElectron from 'is-electron';
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import { Outlet } from 'react-router'; import { Outlet } from 'react-router';
import styled from 'styled-components'; import styled from 'styled-components';
import { UserDetailResponse } from '@/renderer/api/users.api';
import { SideQueue } from '@/renderer/features/side-queue/components/side-queue'; import { SideQueue } from '@/renderer/features/side-queue/components/side-queue';
import { Titlebar } from '@/renderer/features/titlebar/components/titlebar'; import { Titlebar } from '@/renderer/features/titlebar/components/titlebar';
import { useUserDetail } from '@/renderer/features/users'; import { useUserDetail } from '@/renderer/features/users';
@@ -124,7 +125,7 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
{ userId }, { userId },
{ {
cacheTime: Infinity, cacheTime: Infinity,
onSuccess: (res) => { onSuccess: (res: UserDetailResponse) => {
const props = { const props = {
permissions: { permissions: {
id: res.data.id, id: res.data.id,
+9 -16
View File
@@ -4,9 +4,7 @@ import { useAuthStore } from '@/renderer/store';
import { authApi } from '../api/auth.api'; import { authApi } from '../api/auth.api';
export const ax = Axios.create({ export const ax = Axios.create({
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
},
withCredentials: false, withCredentials: false,
}); });
@@ -35,23 +33,19 @@ ax.interceptors.response.use(
async (err) => { async (err) => {
if (err.response && err.response.status === 401) { if (err.response && err.response.status === 401) {
const { config } = err; const { config } = err;
const auth = JSON.parse(
localStorage.getItem('store_authentication') || '{}' const auth = useAuthStore.getState();
);
if (err.response.data.error.message === 'jwt expired' && !config.sent) { if (err.response.data.error.message === 'jwt expired' && !config.sent) {
config.sent = true; config.sent = true;
const { accessToken } = ( const { accessToken } = (
await authApi.refresh(auth.state.serverUrl, { await authApi.refresh(auth.serverUrl, {
refreshToken: auth.refreshToken, refreshToken: auth.refreshToken,
}) })
).data; ).data;
localStorage.setItem( useAuthStore.setState({ ...auth, accessToken });
'store_authentication',
JSON.stringify({ ...auth, state: { ...auth.state, accessToken } })
);
config.headers = { config.headers = {
...config.headers, ...config.headers,
@@ -61,12 +55,11 @@ ax.interceptors.response.use(
return Axios(config); return Axios(config);
} }
const { logout } = useAuthStore.getState(); // if (err.response.data.error.message === 'No auth token') {
if (err.response.data.error.message === 'No auth token') { // auth.logout();
logout(); // }
}
logout(); auth.logout();
} }
return Promise.reject(err); return Promise.reject(err);
} }
+5 -22
View File
@@ -1,12 +1,9 @@
import { import {
QueryClient, QueryClient,
UseQueryOptions, UseQueryOptions,
UseMutationOptions,
DefaultOptions, DefaultOptions,
QueryCache, QueryCache,
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { PromiseValue } from 'type-fest';
import { toast } from '@/renderer/components'; import { toast } from '@/renderer/components';
const queryCache = new QueryCache({ const queryCache = new QueryCache({
@@ -26,7 +23,7 @@ const queryConfig: DefaultOptions = {
onError: (err) => { onError: (err) => {
console.error(err); console.error(err);
}, },
refetchOnWindowFocus: process.env.NODE_ENV === 'production', refetchOnWindowFocus: false,
retry: process.env.NODE_ENV === 'production', retry: process.env.NODE_ENV === 'production',
staleTime: 1000 * 5, staleTime: 1000 * 5,
useErrorBoundary: true, useErrorBoundary: true,
@@ -38,31 +35,17 @@ export const queryClient = new QueryClient({
queryCache, queryCache,
}); });
export type ExtractFnReturnType<FnType extends (...args: any) => any> = export type QueryOptions = {
PromiseValue<ReturnType<FnType>>;
export type QueryConfig<QueryFnType extends (...args: any) => any> = Omit<
UseQueryOptions<ExtractFnReturnType<QueryFnType>>,
'queryKey' | 'queryFn'
>;
export type MutationConfig<MutationFnType extends (...args: any) => any> =
UseMutationOptions<
ExtractFnReturnType<MutationFnType>,
AxiosError,
Parameters<MutationFnType>[0]
>;
export type QueryOptions<TResponse> = {
cacheTime?: UseQueryOptions['cacheTime']; cacheTime?: UseQueryOptions['cacheTime'];
enabled?: UseQueryOptions['enabled']; enabled?: UseQueryOptions['enabled'];
onError?: (err: any) => void; onError?: (err: any) => void;
onSuccess?: (data: any) => void; onSettled?: any;
onSuccess?: any;
refetchInterval?: number; refetchInterval?: number;
refetchIntervalInBackground?: UseQueryOptions['refetchIntervalInBackground']; refetchIntervalInBackground?: UseQueryOptions['refetchIntervalInBackground'];
refetchOnWindowFocus?: boolean; refetchOnWindowFocus?: boolean;
retry?: UseQueryOptions['retry']; retry?: UseQueryOptions['retry'];
retryDelay?: UseQueryOptions['retryDelay']; retryDelay?: UseQueryOptions['retryDelay'];
staleTime?: UseQueryOptions['staleTime']; staleTime?: UseQueryOptions['staleTime'];
useErrorBoundary?: any; useErrorBoundary?: boolean;
}; };