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:
Thomas Camlong
2023-05-17 05:19:14 +09:00
committed by GitHub
parent d0180b1f87
commit 3e6413d9f2
3 changed files with 155 additions and 23 deletions

View File

@@ -13,5 +13,9 @@
"approved": "Approved",
"pendingApproval": "Pending approval",
"declined": "Declined"
},
"tooltips": {
"approve": "Approve requests",
"decline": "Decline requests"
}
}

View File

@@ -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',

View File

@@ -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,23 +118,72 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
</Text>
</Stack>
</Flex>
<Flex gap="xs">
<Image
src={item.userProfilePicture}
width={25}
height={25}
alt="requester avatar"
radius="xl"
withPlaceholder
/>
<Text
component="a"
href={item.userLink}
sx={{ cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
>
{item.userName}
</Text>
</Flex>
<Stack justify="center">
<Flex gap="xs">
<Image
src={item.userProfilePicture}
width={25}
height={25}
alt="requester avatar"
radius="xl"
withPlaceholder
/>
<Text
component="a"
href={item.userLink}
sx={{ cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
>
{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