diff --git a/public/locales/en/modules/media-requests-list.json b/public/locales/en/modules/media-requests-list.json
index 56babff77..f372514bb 100644
--- a/public/locales/en/modules/media-requests-list.json
+++ b/public/locales/en/modules/media-requests-list.json
@@ -13,5 +13,9 @@
"approved": "Approved",
"pendingApproval": "Pending approval",
"declined": "Declined"
+ },
+ "tooltips": {
+ "approve": "Approve requests",
+ "decline": "Decline requests"
}
}
diff --git a/src/pages/api/modules/overseerr/[id].tsx b/src/pages/api/modules/overseerr/[id].tsx
index 55d253400..0335fc5fa 100644
--- a/src/pages/api/modules/overseerr/[id].tsx
+++ b/src/pages/api/modules/overseerr/[id].tsx
@@ -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',
diff --git a/src/widgets/media-requests/MediaRequestListTile.tsx b/src/widgets/media-requests/MediaRequestListTile.tsx
index f840087eb..c9f15e1b0 100644
--- a/src/widgets/media-requests/MediaRequestListTile.tsx
+++ b/src/widgets/media-requests/MediaRequestListTile.tsx
@@ -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 ;
}
@@ -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 (
{countPendingApproval > 0 ? (
@@ -53,7 +90,7 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
) : (
{t('nonePending')}
)}
- {data.map((item) => (
+ {sortedData.map((item) => (
@@ -81,23 +118,72 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
-
-
-
- {item.userName}
-
-
+
+
+
+
+ {item.userName}
+
+
+
+ {item.status === MediaRequestStatus.PendingApproval && (
+
+
+ {
+ 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: ,
+ autoClose: 2000,
+ })
+ );
+
+ await refetch();
+ }}
+ >
+
+
+
+
+ {
+ await mutate.mutateAsync({ request: item, action: 'decline' });
+ await refetch();
+ }}
+ >
+
+
+
+
+ )}
+