Merge pull request #6925 from viown/react-plugins-repositories
Migrate plugin repositories to React
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
export enum QueryKey {
|
||||
ConfigurationPages = 'ConfigurationPages',
|
||||
PackageInfo = 'PackageInfo',
|
||||
Plugins = 'Plugins'
|
||||
Plugins = 'Plugins',
|
||||
Repositories = 'Repositories'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import { queryOptions, useQuery } from '@tanstack/react-query';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { QueryKey } from './queryKey';
|
||||
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api';
|
||||
|
||||
const fetchRepositories = async (
|
||||
api: Api,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const response = await getPackageApi(api)
|
||||
.getRepositories(options);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getRepositoriesQuery = (
|
||||
api?: Api
|
||||
) => queryOptions({
|
||||
queryKey: [ QueryKey.Repositories ],
|
||||
queryFn: ({ signal }) => fetchRepositories(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
|
||||
export const useRepositories = () => {
|
||||
const { api } = useApi();
|
||||
return useQuery(getRepositoriesQuery(api));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { QueryKey } from './queryKey';
|
||||
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api';
|
||||
import { PackageApiSetRepositoriesRequest } from '@jellyfin/sdk/lib/generated-client/api/package-api';
|
||||
|
||||
export const useSetRepositories = () => {
|
||||
const { api } = useApi();
|
||||
return useMutation({
|
||||
mutationFn: (params: PackageApiSetRepositoriesRequest) => (
|
||||
getPackageApi(api!)
|
||||
.setRepositories(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QueryKey.Repositories ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import type { RepositoryInfo } from '@jellyfin/sdk/lib/generated-client/models/repository-info';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import globalize from 'lib/globalize';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import TextField from '@mui/material/TextField';
|
||||
|
||||
type IProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onAdd: (repository: RepositoryInfo) => void;
|
||||
};
|
||||
|
||||
const NewRepositoryForm = ({ open, onClose, onAdd }: IProps) => {
|
||||
const onSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
const repository: RepositoryInfo = {
|
||||
Name: data.Name?.toString(),
|
||||
Url: data.Url?.toString(),
|
||||
Enabled: true
|
||||
};
|
||||
|
||||
onAdd(repository);
|
||||
}, [ onAdd ]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
onClose={onClose}
|
||||
slotProps={{
|
||||
paper: {
|
||||
component: 'form',
|
||||
onSubmit
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle>{globalize.translate('HeaderNewRepository')}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
name='Name'
|
||||
label={globalize.translate('LabelRepositoryName')}
|
||||
helperText={globalize.translate('LabelRepositoryNameHelp')}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
required: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
name='Url'
|
||||
label={globalize.translate('LabelRepositoryUrl')}
|
||||
helperText={globalize.translate('LabelRepositoryUrlHelp')}
|
||||
type='url'
|
||||
/>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant='text'
|
||||
>{globalize.translate('ButtonCancel')}</Button>
|
||||
<Button type='submit'>{globalize.translate('Add')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewRepositoryForm;
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { RepositoryInfo } from '@jellyfin/sdk/lib/generated-client/models/repository-info';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Delete from '@mui/icons-material/Delete';
|
||||
import globalize from 'lib/globalize';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||
import OpenInNew from '@mui/icons-material/OpenInNew';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import Link from '@mui/material/Link';
|
||||
import ConfirmDialog from 'components/ConfirmDialog';
|
||||
|
||||
type IProps = {
|
||||
repository: RepositoryInfo;
|
||||
onDelete: (repository: RepositoryInfo) => void;
|
||||
};
|
||||
|
||||
const RepositoryListItem = ({ repository, onDelete }: IProps) => {
|
||||
const [ isConfirmDeleteOpen, setIsConfirmDeleteOpen ] = useState(false);
|
||||
|
||||
const confirmDeletePrompt = useCallback(() => {
|
||||
setIsConfirmDeleteOpen(true);
|
||||
}, []);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
setIsConfirmDeleteOpen(false);
|
||||
}, []);
|
||||
|
||||
const onConfirmDelete = useCallback(() => {
|
||||
onDelete(repository);
|
||||
setIsConfirmDeleteOpen(false);
|
||||
}, [ onDelete, repository ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmDialog
|
||||
open={isConfirmDeleteOpen}
|
||||
title={globalize.translate('ConfirmDeleteRepository')}
|
||||
text={globalize.translate('DeleteRepositoryConfirmation')}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={onCancel}
|
||||
confirmButtonColor='error'
|
||||
confirmButtonText={globalize.translate('Delete')}
|
||||
/>
|
||||
<ListItem
|
||||
disablePadding
|
||||
secondaryAction={
|
||||
<Tooltip disableInteractive title={globalize.translate('ButtonRemove')}>
|
||||
<IconButton onClick={confirmDeletePrompt}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<ListItemButton>
|
||||
<Link href={repository.Url || '#'} target='_blank' rel='noopener noreferrer'>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: 'primary.main' }}>
|
||||
<OpenInNew sx={{ color: '#fff' }} />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
</Link>
|
||||
<ListItemText
|
||||
primary={repository.Name}
|
||||
secondary={repository.Url}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: 'h3'
|
||||
},
|
||||
secondary: {
|
||||
variant: 'body1'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoryListItem;
|
||||
Reference in New Issue
Block a user