pull/3714/merge
KyleKincer 2 weeks ago committed by GitHub
commit a830257c53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -460,6 +460,9 @@ export class MediaRequest {
@Column({ default: false })
public isAutoRequest: boolean;
@Column({ nullable: true, length: 140 })
public adminMessage?: string;
constructor(init?: Partial<MediaRequest>) {
Object.assign(this, init);
}

@ -19,6 +19,7 @@ export interface NotificationPayload {
request?: MediaRequest;
issue?: Issue;
comment?: IssueComment;
adminMessage?: string;
}
export abstract class BaseAgent<T extends NotificationAgentConfig> {

@ -153,6 +153,14 @@ class DiscordAgent
inline: true,
});
}
if (payload.request.adminMessage) {
fields.push({
name: 'Admin Message',
value: payload.request.adminMessage,
inline: true,
});
}
} else if (payload.comment) {
fields.push({
name: `Comment from ${payload.comment.user.displayName}`,

@ -45,6 +45,9 @@ class WebPushAgent
: 'series'
: undefined;
const is4k = payload.request?.is4k;
const adminMessage = payload.adminMessage
? payload.adminMessage
: undefined;
const issueType = payload.issue
? payload.issue.issueType !== IssueType.OTHER
@ -80,7 +83,9 @@ class WebPushAgent
}${mediaType} request is now available!`;
break;
case Notification.MEDIA_DECLINED:
message = `Your ${is4k ? '4K ' : ''}${mediaType} request was declined.`;
message = `Your ${is4k ? '4K ' : ''}${mediaType} request was declined${
adminMessage ? `: ${adminMessage}` : '.'
}.`;
break;
case Notification.MEDIA_FAILED:
message = `Failed to process ${is4k ? '4K ' : ''}${mediaType} request.`;

@ -0,0 +1,17 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class AddAdminMessage1702596723101 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE media_requests
ADD adminMessage VARCHAR(140)
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE media_requests
DROP COLUMN adminMessage
`);
}
}

@ -538,6 +538,12 @@ requestRoutes.post<{
request.status = newStatus;
request.modifiedBy = req.user;
const adminMessage = req.body.adminMessage;
if (adminMessage) {
request.adminMessage = adminMessage;
}
await requestRepository.save(request);
return res.status(200).json(request);

@ -61,6 +61,10 @@ div(style='display: block; background-color: #111827; padding: 2.5rem 0;')
td(style='font-size: .85em; color: #9ca3af; line-height: 1em; vertical-align: bottom; margin-right: 1rem')
span
| #{timestamp}
if adminMessage
tr
td(style='font-size: 1.0em; color: #9ca3af; line-height: 1.5em; vertical-align: top; margin-right: 1rem')
| #{adminMessage}
if actionUrl
tr
td

@ -0,0 +1,111 @@
import Modal from '@app/components/Common/Modal';
import globalMessages from '@app/i18n/globalMessages';
import { Transition } from '@headlessui/react';
import type { MovieDetails } from '@server/models/Movie';
import { Field, Form, Formik } from 'formik';
import React from 'react';
import { useIntl } from 'react-intl';
import useSWR from 'swr';
import * as Yup from 'yup';
interface DeclineRequestModalProps {
show: boolean;
tmdbId: number;
onDecline: (declineMessage: string) => void;
onCancel?: () => void;
}
const validationSchema = Yup.object().shape({
declineMessage: Yup.string().max(140, 'Message is too long'),
});
const DeclineRequestModal = ({
show,
tmdbId,
onDecline,
onCancel,
}: DeclineRequestModalProps) => {
const intl = useIntl();
const { data, error } = useSWR<MovieDetails>(`/api/v1/movie/${tmdbId}`, {
revalidateOnMount: true,
});
const [characterCount, setCharacterCount] = React.useState(0);
const handleCancel = () => {
setCharacterCount(0);
if (onCancel) {
onCancel();
}
};
return (
<Transition
as="div"
enter="transition-opacity duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={show}
>
<Formik
initialValues={{ declineMessage: '' }}
validationSchema={validationSchema}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(true);
onDecline(values.declineMessage);
}}
>
{({ errors, touched, handleSubmit, setFieldValue, values }) => {
const handleInputChange = (
event: React.ChangeEvent<HTMLTextAreaElement>
) => {
const { value } = event.target;
setFieldValue('declineMessage', value);
setCharacterCount(value.length);
};
return (
<Modal
loading={!data && !error}
title="Decline Request"
subTitle={data?.title}
onCancel={handleCancel}
onOk={() => {
handleSubmit();
}}
okText={intl.formatMessage(globalMessages.decline)}
okButtonType="danger"
cancelText={intl.formatMessage(globalMessages.cancel)}
backdrop={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data?.backdropPath}`}
>
<Form>
<Field
name="declineMessage"
as="textarea"
placeholder="Optional decline message"
onChange={handleInputChange}
value={values.declineMessage}
/>
{errors.declineMessage && touched.declineMessage ? (
<div className="pt-2 text-red-500">
{errors.declineMessage}
</div>
) : null}
<div
className={`pt-2 text-xs font-light ${
characterCount > 140 ? 'text-red-500' : 'text-gray-300'
}`}
>
{characterCount}/140
</div>
</Form>
</Modal>
);
}}
</Formik>
</Transition>
);
};
export default DeclineRequestModal;

@ -2,6 +2,7 @@ import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import ConfirmButton from '@app/components/Common/ConfirmButton';
import DeclineRequestModal from '@app/components/DeclineRequestModal';
import RequestModal from '@app/components/RequestModal';
import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
@ -283,6 +284,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
const intl = useIntl();
const { user, hasPermission } = useUser();
const [showEditModal, setShowEditModal] = useState(false);
const [showDeclineModal, setShowDeclineModal] = useState(false);
const url =
request.type === 'movie'
? `/api/v1/movie/${request.media.tmdbId}`
@ -306,6 +308,16 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
const [isRetrying, setRetrying] = useState(false);
const declineRequest = async (declineMessage: string) => {
const response = await axios.post(`/api/v1/request/${request.id}/decline`, {
adminMessage: declineMessage,
});
if (response) {
revalidate();
}
};
const modifyRequest = async (type: 'approve' | 'decline') => {
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
@ -375,7 +387,16 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
setShowEditModal(false);
}}
/>
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
<DeclineRequestModal
show={showDeclineModal}
tmdbId={request.media.tmdbId}
onDecline={(declineMessage) => {
declineRequest(declineMessage);
setShowDeclineModal(false);
}}
onCancel={() => setShowDeclineModal(false)}
/>
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:flex-row">
{title.backdropPath && (
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
<CachedImage
@ -598,6 +619,17 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
</span>
</div>
)}
{requestData.adminMessage && (
<div className="card-field">
<span className="card-field-name">
{requestData.status === MediaRequestStatus.DECLINED && (
<span className="flex truncate whitespace-normal text-sm font-light italic text-gray-300">
&quot;{requestData.adminMessage}&quot;
</span>
)}
</span>
</div>
)}
</div>
</div>
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
@ -648,7 +680,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
<Button
className="w-full"
buttonType="danger"
onClick={() => modifyRequest('decline')}
onClick={() => setShowDeclineModal(true)}
>
<XMarkIcon />
<span>{intl.formatMessage(globalMessages.decline)}</span>

Loading…
Cancel
Save