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) { - - requester avatar - - {item.userName} - - + + + requester avatar + + {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(); + }} + > + + + + + )} +