Merge pull request #6925 from viown/react-plugins-repositories

Migrate plugin repositories to React
This commit is contained in:
Bill Thornton
2025-06-10 18:27:50 -04:00
committed by GitHub
11 changed files with 329 additions and 217 deletions
@@ -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;