mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-11 07:55:52 +01:00
✨Ability to manage media requests from the widget (#894)
* ✨Ability to manage media requests from the widget * 🚸 Improve UX & Design --------- Co-authored-by: Manuel <manuel.ruwe@bluewin.ch>
This commit is contained in:
@@ -13,5 +13,9 @@
|
||||
"approved": "Approved",
|
||||
"pendingApproval": "Pending approval",
|
||||
"declined": "Declined"
|
||||
},
|
||||
"tooltips": {
|
||||
"approve": "Approve requests",
|
||||
"decline": "Decline requests"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,45 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
|
||||
);
|
||||
}
|
||||
|
||||
async function Put(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Get the slug of the request
|
||||
const { id, action } = req.query as { id: string; action: string };
|
||||
const configName = getCookie('config-name', { req });
|
||||
const config = getConfig(configName?.toString() ?? 'default');
|
||||
Consola.log('Got a request to approve or decline a request', id, action);
|
||||
const app = config.apps.find(
|
||||
(app) => app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
|
||||
);
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: 'No id provided' });
|
||||
}
|
||||
if (action !== 'approve' && action !== 'decline') {
|
||||
return res.status(400).json({ error: 'Action type undefined' });
|
||||
}
|
||||
|
||||
const apiKey = app?.integration?.properties.find((x) => x.field === 'apiKey')?.value;
|
||||
if (!apiKey) {
|
||||
return res.status(400).json({ error: 'No app found' });
|
||||
}
|
||||
const appUrl = new URL(app.url);
|
||||
return axios
|
||||
.post(
|
||||
`${appUrl.origin}/api/v1/request/${id}/${action}`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'X-Api-Key': apiKey,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((axiosres) => res.status(200).json(axiosres.data))
|
||||
.catch((err) =>
|
||||
res.status(500).json({
|
||||
message: err.message,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'POST') {
|
||||
return Post(req, res);
|
||||
@@ -125,6 +164,9 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'GET') {
|
||||
return Get(req, res);
|
||||
}
|
||||
if (req.method === 'PUT') {
|
||||
return Put(req, res);
|
||||
}
|
||||
return res.status(405).json({
|
||||
statusCode: 405,
|
||||
message: 'Method not allowed',
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import { Badge, Card, Center, Flex, Group, Image, Stack, Text } from '@mantine/core';
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Card,
|
||||
Center,
|
||||
Flex,
|
||||
Group,
|
||||
Image,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { IconGitPullRequest } from '@tabler/icons-react';
|
||||
import { IconCheck, IconGitPullRequest, IconThumbDown, IconThumbUp } from '@tabler/icons-react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { defineWidget } from '../helper';
|
||||
import { WidgetLoading } from '../loading';
|
||||
import { IWidget } from '../widgets';
|
||||
import { useMediaRequestQuery } from './media-request-query';
|
||||
import { MediaRequestStatus } from './media-request-types';
|
||||
import { MediaRequest, MediaRequestStatus } from './media-request-types';
|
||||
|
||||
const definition = defineWidget({
|
||||
id: 'media-requests-list',
|
||||
@@ -28,9 +42,21 @@ interface MediaRequestListWidgetProps {
|
||||
|
||||
function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
||||
const { t } = useTranslation('modules/media-requests-list');
|
||||
const { data, isFetching } = useMediaRequestQuery();
|
||||
const { data, refetch, isLoading } = useMediaRequestQuery();
|
||||
// Use mutation to approve or deny a pending request
|
||||
const mutate = useMutation({
|
||||
mutationFn: async (e: { request: MediaRequest; action: string }) => {
|
||||
const data = await axios.put(`/api/modules/overseerr/${e.request.id}?action=${e.action}`);
|
||||
notifications.show({
|
||||
title: t('requestUpdated'),
|
||||
message: t('requestUpdatedMessage', { title: e.request.name }),
|
||||
color: 'blue',
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
if (!data || isFetching) {
|
||||
if (!data || isLoading) {
|
||||
return <WidgetLoading />;
|
||||
}
|
||||
|
||||
@@ -46,6 +72,17 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
||||
(x) => x.status === MediaRequestStatus.PendingApproval
|
||||
).length;
|
||||
|
||||
// Return a sorted data by status to show pending first, then the default order
|
||||
const sortedData = data.sort((a: MediaRequest, b: MediaRequest) => {
|
||||
if (a.status === MediaRequestStatus.PendingApproval) {
|
||||
return -1;
|
||||
}
|
||||
if (b.status === MediaRequestStatus.PendingApproval) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{countPendingApproval > 0 ? (
|
||||
@@ -53,7 +90,7 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
||||
) : (
|
||||
<Text>{t('nonePending')}</Text>
|
||||
)}
|
||||
{data.map((item) => (
|
||||
{sortedData.map((item) => (
|
||||
<Card pos="relative" withBorder>
|
||||
<Flex justify="space-between" gap="md">
|
||||
<Flex gap="md">
|
||||
@@ -81,6 +118,7 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
||||
</Text>
|
||||
</Stack>
|
||||
</Flex>
|
||||
<Stack justify="center">
|
||||
<Flex gap="xs">
|
||||
<Image
|
||||
src={item.userProfilePicture}
|
||||
@@ -98,6 +136,54 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
||||
{item.userName}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{item.status === MediaRequestStatus.PendingApproval && (
|
||||
<Group>
|
||||
<Tooltip label={t('tooltips.approve')} withArrow withinPortal>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={async () => {
|
||||
notifications.show({
|
||||
id: `approve ${item.id}`,
|
||||
color: 'yellow',
|
||||
title: 'Approving request...',
|
||||
message: undefined,
|
||||
loading: true,
|
||||
});
|
||||
|
||||
await mutate.mutateAsync({ request: item, action: 'approve' }).then(() =>
|
||||
notifications.update({
|
||||
id: `approve ${item.id}`,
|
||||
color: 'teal',
|
||||
title: 'Request was approved!',
|
||||
message: undefined,
|
||||
icon: <IconCheck size="1rem" />,
|
||||
autoClose: 2000,
|
||||
})
|
||||
);
|
||||
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
<IconThumbUp />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('tooltips.decline')} withArrow withinPortal>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={async () => {
|
||||
await mutate.mutateAsync({ request: item, action: 'decline' });
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
<IconThumbDown />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</Flex>
|
||||
|
||||
<Image
|
||||
|
||||
Reference in New Issue
Block a user