mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-11 16:05:47 +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",
|
"approved": "Approved",
|
||||||
"pendingApproval": "Pending approval",
|
"pendingApproval": "Pending approval",
|
||||||
"declined": "Declined"
|
"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) => {
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
return Post(req, res);
|
return Post(req, res);
|
||||||
@@ -125,6 +164,9 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
return Get(req, res);
|
return Get(req, res);
|
||||||
}
|
}
|
||||||
|
if (req.method === 'PUT') {
|
||||||
|
return Put(req, res);
|
||||||
|
}
|
||||||
return res.status(405).json({
|
return res.status(405).json({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed',
|
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 { 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 { defineWidget } from '../helper';
|
||||||
import { WidgetLoading } from '../loading';
|
import { WidgetLoading } from '../loading';
|
||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
import { useMediaRequestQuery } from './media-request-query';
|
import { useMediaRequestQuery } from './media-request-query';
|
||||||
import { MediaRequestStatus } from './media-request-types';
|
import { MediaRequest, MediaRequestStatus } from './media-request-types';
|
||||||
|
|
||||||
const definition = defineWidget({
|
const definition = defineWidget({
|
||||||
id: 'media-requests-list',
|
id: 'media-requests-list',
|
||||||
@@ -28,9 +42,21 @@ interface MediaRequestListWidgetProps {
|
|||||||
|
|
||||||
function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
||||||
const { t } = useTranslation('modules/media-requests-list');
|
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 />;
|
return <WidgetLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +72,17 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
|||||||
(x) => x.status === MediaRequestStatus.PendingApproval
|
(x) => x.status === MediaRequestStatus.PendingApproval
|
||||||
).length;
|
).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 (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
{countPendingApproval > 0 ? (
|
{countPendingApproval > 0 ? (
|
||||||
@@ -53,7 +90,7 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
|||||||
) : (
|
) : (
|
||||||
<Text>{t('nonePending')}</Text>
|
<Text>{t('nonePending')}</Text>
|
||||||
)}
|
)}
|
||||||
{data.map((item) => (
|
{sortedData.map((item) => (
|
||||||
<Card pos="relative" withBorder>
|
<Card pos="relative" withBorder>
|
||||||
<Flex justify="space-between" gap="md">
|
<Flex justify="space-between" gap="md">
|
||||||
<Flex gap="md">
|
<Flex gap="md">
|
||||||
@@ -81,23 +118,72 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex gap="xs">
|
<Stack justify="center">
|
||||||
<Image
|
<Flex gap="xs">
|
||||||
src={item.userProfilePicture}
|
<Image
|
||||||
width={25}
|
src={item.userProfilePicture}
|
||||||
height={25}
|
width={25}
|
||||||
alt="requester avatar"
|
height={25}
|
||||||
radius="xl"
|
alt="requester avatar"
|
||||||
withPlaceholder
|
radius="xl"
|
||||||
/>
|
withPlaceholder
|
||||||
<Text
|
/>
|
||||||
component="a"
|
<Text
|
||||||
href={item.userLink}
|
component="a"
|
||||||
sx={{ cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
|
href={item.userLink}
|
||||||
>
|
sx={{ cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
|
||||||
{item.userName}
|
>
|
||||||
</Text>
|
{item.userName}
|
||||||
</Flex>
|
</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>
|
</Flex>
|
||||||
|
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
Reference in New Issue
Block a user