fix: set editRequest attribute as necessary, allow users to edit their own pending requests, and show 'View Request' button on series pages (#1446)

* fix: set editRequest attribute for RequestModal

* fix: remove now-unneeded conditional

* fix(ui): only show 'View Request' for user's own requests if they don't have MANAGE_REQUESTS perm

* fix(ui): show edit button on request list for own requests & 'View Request' button on series pages

* fix(ui): do not show 'Request More' if user already has a pending request

* fix: address PR comments

* fix(lang): edit usercreatedfaileexisting string & generate translation key

* fix: users should always be able to view/edit their own requests even if their perms have changed

also fixed capitalization of 'Signing In...' string
pull/1470/head
TheCatLady 3 years ago committed by GitHub
parent f13f1c9451
commit 89455ad9b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -493,7 +493,6 @@ requestRoutes.get('/:requestId', async (req, res, next) => {
requestRoutes.put<{ requestId: string }>( requestRoutes.put<{ requestId: string }>(
'/:requestId', '/:requestId',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => { async (req, res, next) => {
const requestRepository = getRepository(MediaRequest); const requestRepository = getRepository(MediaRequest);
const userRepository = getRepository(User); const userRepository = getRepository(User);
@ -503,17 +502,30 @@ requestRoutes.put<{ requestId: string }>(
); );
if (!request) { if (!request) {
return next({ status: 404, message: 'Request not found' }); return next({ status: 404, message: 'Request not found.' });
}
if (
(request.requestedBy.id !== req.user?.id ||
(req.body.mediaType !== 'tv' &&
!req.user?.hasPermission(Permission.REQUEST_ADVANCED))) &&
!req.user?.hasPermission(Permission.MANAGE_REQUESTS)
) {
return next({
status: 403,
message: 'You do not have permission to modify this request.',
});
} }
let requestUser = req.user; let requestUser = req.user;
if ( if (
req.body.userId && req.body.userId &&
!( req.body.userId !== req.user?.id &&
req.user?.hasPermission(Permission.MANAGE_USERS) && !req.user?.hasPermission([
req.user?.hasPermission(Permission.MANAGE_REQUESTS) Permission.MANAGE_USERS,
) Permission.MANAGE_REQUESTS,
])
) { ) {
return next({ return next({
status: 403, status: 403,
@ -546,7 +558,7 @@ requestRoutes.put<{ requestId: string }>(
if (!requestedSeasons || requestedSeasons.length === 0) { if (!requestedSeasons || requestedSeasons.length === 0) {
throw new Error( throw new Error(
'Missing seasons. If you want to cancel a tv request, use the DELETE method.' 'Missing seasons. If you want to cancel a series request, use the DELETE method.'
); );
} }
@ -633,7 +645,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
) { ) {
return next({ return next({
status: 401, status: 401,
message: 'You do not have permission to remove this request', message: 'You do not have permission to delete this request.',
}); });
} }
@ -642,7 +654,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
return res.status(204).send(); return res.status(204).send();
} catch (e) { } catch (e) {
logger.error(e.message); logger.error(e.message);
next({ status: 404, message: 'Request not found' }); next({ status: 404, message: 'Request not found.' });
} }
}); });
@ -668,7 +680,7 @@ requestRoutes.post<{
label: 'Media Request', label: 'Media Request',
message: e.message, message: e.message,
}); });
next({ status: 404, message: 'Request not found' }); next({ status: 404, message: 'Request not found.' });
} }
} }
); );
@ -712,7 +724,7 @@ requestRoutes.post<{
label: 'Media Request', label: 'Media Request',
message: e.message, message: e.message,
}); });
next({ status: 404, message: 'Request not found' }); next({ status: 404, message: 'Request not found.' });
} }
} }
); );

@ -13,7 +13,7 @@ const messages = defineMessages({
validationemailrequired: 'You must provide a valid email address', validationemailrequired: 'You must provide a valid email address',
validationpasswordrequired: 'You must provide a password', validationpasswordrequired: 'You must provide a password',
loginerror: 'Something went wrong while trying to sign in.', loginerror: 'Something went wrong while trying to sign in.',
signingin: 'Signing in…', signingin: 'Signing In…',
signin: 'Sign In', signin: 'Sign In',
forgotpassword: 'Forgot Password?', forgotpassword: 'Forgot Password?',
}); });

@ -6,7 +6,7 @@ import PlexOAuth from '../../utils/plex';
const messages = defineMessages({ const messages = defineMessages({
signinwithplex: 'Sign In', signinwithplex: 'Sign In',
signingin: 'Signing in…', signingin: 'Signing In…',
}); });
const plexOAuth = new PlexOAuth(); const plexOAuth = new PlexOAuth();

@ -5,7 +5,7 @@ import {
XIcon, XIcon,
} from '@heroicons/react/solid'; } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { import {
MediaRequestStatus, MediaRequestStatus,
@ -23,19 +23,19 @@ const messages = defineMessages({
viewrequest: 'View Request', viewrequest: 'View Request',
viewrequest4k: 'View 4K Request', viewrequest4k: 'View 4K Request',
requestmore: 'Request More', requestmore: 'Request More',
requestmore4k: 'Request More 4K', requestmore4k: 'Request More in 4K',
approverequest: 'Approve Request', approverequest: 'Approve Request',
approverequest4k: 'Approve 4K Request', approverequest4k: 'Approve 4K Request',
declinerequest: 'Decline Request', declinerequest: 'Decline Request',
declinerequest4k: 'Decline 4K Request', declinerequest4k: 'Decline 4K Request',
approverequests: approverequests:
'Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}', 'Approve {requestCount, plural, one {Request} other {{requestCount} Requests}}',
declinerequests: declinerequests:
'Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}', 'Decline {requestCount, plural, one {Request} other {{requestCount} Requests}}',
approve4krequests: approve4krequests:
'Approve {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}', 'Approve {requestCount, plural, one {Request} other {{requestCount} 4K Requests}}',
decline4krequests: decline4krequests:
'Decline {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}', 'Decline {requestCount, plural, one {Request} other {{requestCount} 4K Requests}}',
}); });
interface ButtonOption { interface ButtonOption {
@ -64,26 +64,34 @@ const RequestButton: React.FC<RequestButtonProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const settings = useSettings(); const settings = useSettings();
const { hasPermission } = useUser(); const { user, hasPermission } = useUser();
const [showRequestModal, setShowRequestModal] = useState(false); const [showRequestModal, setShowRequestModal] = useState(false);
const [showRequest4kModal, setShowRequest4kModal] = useState(false); const [showRequest4kModal, setShowRequest4kModal] = useState(false);
const [editRequest, setEditRequest] = useState(false);
const activeRequest = media?.requests.find( // All pending requests
(request) => request.status === MediaRequestStatus.PENDING && !request.is4k
);
const active4kRequest = media?.requests.find(
(request) => request.status === MediaRequestStatus.PENDING && request.is4k
);
// All pending
const activeRequests = media?.requests.filter( const activeRequests = media?.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING && !request.is4k (request) => request.status === MediaRequestStatus.PENDING && !request.is4k
); );
const active4kRequests = media?.requests.filter( const active4kRequests = media?.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING && request.is4k (request) => request.status === MediaRequestStatus.PENDING && request.is4k
); );
const activeRequest = useMemo(() => {
return activeRequests && activeRequests.length > 0
? activeRequests.find((request) => request.requestedBy.id === user?.id) ??
activeRequests[0]
: undefined;
}, [activeRequests, user]);
const active4kRequest = useMemo(() => {
return active4kRequests && active4kRequests.length > 0
? active4kRequests.find(
(request) => request.requestedBy.id === user?.id
) ?? active4kRequests[0]
: undefined;
}, [active4kRequests, user]);
const modifyRequest = async ( const modifyRequest = async (
request: MediaRequest, request: MediaRequest,
type: 'approve' | 'decline' type: 'approve' | 'decline'
@ -121,24 +129,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
id: 'request', id: 'request',
text: intl.formatMessage(globalMessages.request), text: intl.formatMessage(globalMessages.request),
action: () => { action: () => {
setShowRequestModal(true); setEditRequest(false);
},
svg: <DownloadIcon className="w-5 h-5 mr-1" />,
});
}
if (
hasPermission(Permission.REQUEST) &&
mediaType === 'tv' &&
media &&
media.status !== MediaStatus.AVAILABLE &&
media.status !== MediaStatus.UNKNOWN &&
!isShowComplete
) {
buttons.push({
id: 'request-more',
text: intl.formatMessage(messages.requestmore),
action: () => {
setShowRequestModal(true); setShowRequestModal(true);
}, },
svg: <DownloadIcon className="w-5 h-5 mr-1" />, svg: <DownloadIcon className="w-5 h-5 mr-1" />,
@ -157,26 +148,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
id: 'request4k', id: 'request4k',
text: intl.formatMessage(globalMessages.request4k), text: intl.formatMessage(globalMessages.request4k),
action: () => { action: () => {
setShowRequest4kModal(true); setEditRequest(false);
},
svg: <DownloadIcon className="w-5 h-5 mr-1" />,
});
}
if (
mediaType === 'tv' &&
(hasPermission(Permission.REQUEST_4K) ||
(mediaType === 'tv' && hasPermission(Permission.REQUEST_4K_TV))) &&
media &&
media.status4k !== MediaStatus.AVAILABLE &&
media.status4k !== MediaStatus.UNKNOWN &&
!is4kShowComplete &&
settings.currentSettings.series4kEnabled
) {
buttons.push({
id: 'request-more-4k',
text: intl.formatMessage(messages.requestmore4k),
action: () => {
setShowRequest4kModal(true); setShowRequest4kModal(true);
}, },
svg: <DownloadIcon className="w-5 h-5 mr-1" />, svg: <DownloadIcon className="w-5 h-5 mr-1" />,
@ -185,27 +157,34 @@ const RequestButton: React.FC<RequestButtonProps> = ({
if ( if (
activeRequest && activeRequest &&
mediaType === 'movie' && (activeRequest.requestedBy.id === user?.id ||
hasPermission(Permission.REQUEST) (activeRequests?.length === 1 &&
hasPermission(Permission.MANAGE_REQUESTS)))
) { ) {
buttons.push({ buttons.push({
id: 'active-request', id: 'active-request',
text: intl.formatMessage(messages.viewrequest), text: intl.formatMessage(messages.viewrequest),
action: () => setShowRequestModal(true), action: () => {
setEditRequest(true);
setShowRequestModal(true);
},
svg: <InformationCircleIcon className="w-5 h-5 mr-1" />, svg: <InformationCircleIcon className="w-5 h-5 mr-1" />,
}); });
} }
if ( if (
active4kRequest && active4kRequest &&
mediaType === 'movie' && (active4kRequest.requestedBy.id === user?.id ||
(hasPermission(Permission.REQUEST_4K) || (active4kRequests?.length === 1 &&
hasPermission(Permission.REQUEST_4K_MOVIE)) hasPermission(Permission.MANAGE_REQUESTS)))
) { ) {
buttons.push({ buttons.push({
id: 'active-4k-request', id: 'active-4k-request',
text: intl.formatMessage(messages.viewrequest4k), text: intl.formatMessage(messages.viewrequest4k),
action: () => setShowRequest4kModal(true), action: () => {
setEditRequest(true);
setShowRequest4kModal(true);
},
svg: <InformationCircleIcon className="w-5 h-5 mr-1" />, svg: <InformationCircleIcon className="w-5 h-5 mr-1" />,
}); });
} }
@ -320,6 +299,49 @@ const RequestButton: React.FC<RequestButtonProps> = ({
); );
} }
if (
mediaType === 'tv' &&
(!activeRequest || activeRequest.requestedBy.id !== user?.id) &&
hasPermission(Permission.REQUEST) &&
media &&
media.status !== MediaStatus.AVAILABLE &&
media.status !== MediaStatus.UNKNOWN &&
!isShowComplete
) {
buttons.push({
id: 'request-more',
text: intl.formatMessage(messages.requestmore),
action: () => {
setEditRequest(false);
setShowRequestModal(true);
},
svg: <DownloadIcon className="w-5 h-5 mr-1" />,
});
}
if (
mediaType === 'tv' &&
(!active4kRequest || active4kRequest.requestedBy.id !== user?.id) &&
hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], {
type: 'or',
}) &&
media &&
media.status4k !== MediaStatus.AVAILABLE &&
media.status4k !== MediaStatus.UNKNOWN &&
!is4kShowComplete &&
settings.currentSettings.series4kEnabled
) {
buttons.push({
id: 'request-more-4k',
text: intl.formatMessage(messages.requestmore4k),
action: () => {
setEditRequest(false);
setShowRequest4kModal(true);
},
svg: <DownloadIcon className="w-5 h-5 mr-1" />,
});
}
const [buttonOne, ...others] = buttons; const [buttonOne, ...others] = buttons;
if (!buttonOne) { if (!buttonOne) {
@ -332,6 +354,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
tmdbId={tmdbId} tmdbId={tmdbId}
show={showRequestModal} show={showRequestModal}
type={mediaType} type={mediaType}
editRequest={editRequest ? activeRequest : undefined}
onComplete={() => { onComplete={() => {
onUpdate(); onUpdate();
setShowRequestModal(false); setShowRequestModal(false);
@ -342,6 +365,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
tmdbId={tmdbId} tmdbId={tmdbId}
show={showRequest4kModal} show={showRequest4kModal}
type={mediaType} type={mediaType}
editRequest={editRequest ? active4kRequest : undefined}
is4k is4k
onComplete={() => { onComplete={() => {
onUpdate(); onUpdate();

@ -36,6 +36,7 @@ const messages = defineMessages({
modified: 'Modified', modified: 'Modified',
modifieduserdate: '{date} by {user}', modifieduserdate: '{date} by {user}',
mediaerror: 'The associated title for this request is no longer available.', mediaerror: 'The associated title for this request is no longer available.',
editrequest: 'Edit Request',
deleterequest: 'Delete Request', deleterequest: 'Delete Request',
cancelRequest: 'Cancel Request', cancelRequest: 'Cancel Request',
}); });
@ -363,20 +364,6 @@ const RequestItem: React.FC<RequestItemProps> = ({
</div> </div>
</div> </div>
<div className="z-10 flex flex-col justify-center w-full pl-4 pr-4 mt-4 space-y-2 xl:mt-0 xl:items-end xl:w-96 xl:pl-0"> <div className="z-10 flex flex-col justify-center w-full pl-4 pr-4 mt-4 space-y-2 xl:mt-0 xl:items-end xl:w-96 xl:pl-0">
{requestData.status === MediaRequestStatus.PENDING &&
!hasPermission(Permission.MANAGE_REQUESTS) &&
requestData.requestedBy.id === user?.id && (
<ConfirmButton
onClick={() => deleteRequest()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<XIcon className="w-5 h-5 mr-1" />
<span className="block">
{intl.formatMessage(messages.cancelRequest)}
</span>
</ConfirmButton>
)}
{requestData.media[requestData.is4k ? 'status4k' : 'status'] === {requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN && MediaStatus.UNKNOWN &&
requestData.status !== MediaRequestStatus.DECLINED && requestData.status !== MediaRequestStatus.DECLINED &&
@ -407,52 +394,70 @@ const RequestItem: React.FC<RequestItemProps> = ({
> >
<TrashIcon className="w-5 h-5 mr-1" /> <TrashIcon className="w-5 h-5 mr-1" />
<span className="block"> <span className="block">
{intl.formatMessage(globalMessages.delete)} {intl.formatMessage(messages.deleterequest)}
</span> </span>
</ConfirmButton> </ConfirmButton>
)} )}
{requestData.status === MediaRequestStatus.PENDING && {requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && ( hasPermission(Permission.MANAGE_REQUESTS) && (
<> <div className="flex flex-row w-full space-x-2">
<div className="flex flex-row w-full space-x-2">
<span className="w-full">
<Button
className="w-full"
buttonType="success"
onClick={() => modifyRequest('approve')}
>
<CheckIcon className="w-5 h-5 mr-1" />
<span className="block">
{intl.formatMessage(globalMessages.approve)}
</span>
</Button>
</span>
<span className="w-full">
<Button
className="w-full"
buttonType="danger"
onClick={() => modifyRequest('decline')}
>
<XIcon className="w-5 h-5 mr-1" />
<span className="block">
{intl.formatMessage(globalMessages.decline)}
</span>
</Button>
</span>
</div>
<span className="w-full"> <span className="w-full">
<Button <Button
className="w-full" className="w-full"
buttonType="primary" buttonType="success"
onClick={() => setShowEditModal(true)} onClick={() => modifyRequest('approve')}
> >
<PencilIcon className="w-5 h-5 mr-1" /> <CheckIcon className="w-5 h-5 mr-1" />
<span className="block"> <span className="block">
{intl.formatMessage(globalMessages.edit)} {intl.formatMessage(globalMessages.approve)}
</span> </span>
</Button> </Button>
</span> </span>
</> <span className="w-full">
<Button
className="w-full"
buttonType="danger"
onClick={() => modifyRequest('decline')}
>
<XIcon className="w-5 h-5 mr-1" />
<span className="block">
{intl.formatMessage(globalMessages.decline)}
</span>
</Button>
</span>
</div>
)}
{requestData.status === MediaRequestStatus.PENDING &&
(hasPermission(Permission.MANAGE_REQUESTS) ||
(requestData.requestedBy.id === user?.id &&
(requestData.type === 'tv' ||
hasPermission(Permission.REQUEST_ADVANCED)))) && (
<span className="w-full">
<Button
className="w-full"
buttonType="primary"
onClick={() => setShowEditModal(true)}
>
<PencilIcon className="w-5 h-5 mr-1" />
<span className="block">
{intl.formatMessage(messages.editrequest)}
</span>
</Button>
</span>
)}
{requestData.status === MediaRequestStatus.PENDING &&
!hasPermission(Permission.MANAGE_REQUESTS) &&
requestData.requestedBy.id === user?.id && (
<ConfirmButton
onClick={() => deleteRequest()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<XIcon className="w-5 h-5 mr-1" />
<span className="block">
{intl.formatMessage(messages.cancelRequest)}
</span>
</ConfirmButton>
)} )}
</div> </div>
</div> </div>

@ -4,10 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import { import { MediaStatus } from '../../../server/constants/media';
MediaRequestStatus,
MediaStatus,
} from '../../../server/constants/media';
import { MediaRequest } from '../../../server/entity/MediaRequest'; import { MediaRequest } from '../../../server/entity/MediaRequest';
import { QuotaResponse } from '../../../server/interfaces/api/userInterfaces'; import { QuotaResponse } from '../../../server/interfaces/api/userInterfaces';
import { Permission } from '../../../server/lib/permissions'; import { Permission } from '../../../server/lib/permissions';
@ -25,11 +22,11 @@ const messages = defineMessages({
requestCancel: 'Request for <strong>{title}</strong> canceled.', requestCancel: 'Request for <strong>{title}</strong> canceled.',
requesttitle: 'Request {title}', requesttitle: 'Request {title}',
request4ktitle: 'Request {title} in 4K', request4ktitle: 'Request {title} in 4K',
edit: 'Edit Request',
cancel: 'Cancel Request', cancel: 'Cancel Request',
pendingrequest: 'Pending Request for {title}', pendingrequest: 'Pending Request for {title}',
pending4krequest: 'Pending Request for {title} in 4K', pending4krequest: 'Pending 4K Request for {title}',
requestfrom: 'There is currently a pending request from {username}.', requestfrom: "{username}'s request is pending approval.",
request4kfrom: 'There is currently a pending 4K request from {username}.',
errorediting: 'Something went wrong while editing the request.', errorediting: 'Something went wrong while editing the request.',
requestedited: 'Request for <strong>{title}</strong> edited successfully!', requestedited: 'Request for <strong>{title}</strong> edited successfully!',
requesterror: 'Something went wrong while submitting the request.', requesterror: 'Something went wrong while submitting the request.',
@ -130,18 +127,14 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
} finally { } finally {
setIsUpdating(false); setIsUpdating(false);
} }
}, [data, onComplete, addToast, requestOverrides]); }, [data, onComplete, addToast, requestOverrides, hasPermission, intl, is4k]);
const activeRequest = data?.mediaInfo?.requests?.find(
(request) => request.is4k === !!is4k
);
const cancelRequest = async () => { const cancelRequest = async () => {
setIsUpdating(true); setIsUpdating(true);
try { try {
const response = await axios.delete<MediaRequest>( const response = await axios.delete<MediaRequest>(
`/api/v1/request/${activeRequest?.id}` `/api/v1/request/${editRequest?.id}`
); );
if (response.status === 204) { if (response.status === 204) {
@ -206,11 +199,15 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
} }
}; };
const isOwner = activeRequest if (editRequest) {
? activeRequest.requestedBy.id === user?.id const isOwner = editRequest.requestedBy.id === user?.id;
: false; const showEditButton = hasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_ADVANCED],
{
type: 'or',
}
);
if (activeRequest?.status === MediaRequestStatus.PENDING) {
return ( return (
<Modal <Modal
loading={!data && !error} loading={!data && !error}
@ -218,48 +215,47 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
onCancel={onCancel} onCancel={onCancel}
title={intl.formatMessage( title={intl.formatMessage(
is4k ? messages.pending4krequest : messages.pendingrequest, is4k ? messages.pending4krequest : messages.pendingrequest,
{ { title: data?.title }
title: data?.title,
}
)} )}
onOk={() => (isOwner ? cancelRequest() : updateRequest())} onOk={() => (showEditButton ? updateRequest() : cancelRequest())}
okDisabled={isUpdating} okDisabled={isUpdating}
okText={ okText={
isOwner showEditButton
? isUpdating ? intl.formatMessage(messages.edit)
? intl.formatMessage(globalMessages.canceling) : intl.formatMessage(messages.cancel)
: intl.formatMessage(messages.cancel)
: intl.formatMessage(globalMessages.edit)
} }
okButtonType={isOwner ? 'danger' : 'primary'} okButtonType={showEditButton ? 'primary' : 'danger'}
onSecondary={
isOwner && showEditButton ? () => cancelRequest() : undefined
}
secondaryDisabled={isUpdating}
secondaryText={
isOwner && showEditButton
? intl.formatMessage(messages.cancel)
: undefined
}
secondaryButtonType="danger"
cancelText={intl.formatMessage(globalMessages.close)} cancelText={intl.formatMessage(globalMessages.close)}
iconSvg={<DownloadIcon className="w-6 h-6" />} iconSvg={<DownloadIcon className="w-6 h-6" />}
> >
{isOwner {isOwner
? intl.formatMessage(messages.pendingapproval) ? intl.formatMessage(messages.pendingapproval)
: intl.formatMessage( : intl.formatMessage(messages.requestfrom, {
is4k ? messages.request4kfrom : messages.requestfrom, username: editRequest.requestedBy.displayName,
{ })}
username: activeRequest.requestedBy.displayName,
}
)}
{(hasPermission(Permission.REQUEST_ADVANCED) || {(hasPermission(Permission.REQUEST_ADVANCED) ||
hasPermission(Permission.MANAGE_REQUESTS)) && ( hasPermission(Permission.MANAGE_REQUESTS)) && (
<div className="mt-4"> <div className="mt-4">
<AdvancedRequester <AdvancedRequester
type="movie" type="movie"
is4k={is4k} is4k={is4k}
requestUser={editRequest?.requestedBy} requestUser={editRequest.requestedBy}
defaultOverrides={ defaultOverrides={{
editRequest folder: editRequest.rootFolder,
? { profile: editRequest.profileId,
folder: editRequest.rootFolder, server: editRequest.serverId,
profile: editRequest.profileId, tags: editRequest.tags,
server: editRequest.serverId, }}
tags: editRequest.tags,
}
: undefined
}
onChange={(overrides) => { onChange={(overrides) => {
setRequestOverrides(overrides); setRequestOverrides(overrides);
}} }}

@ -29,6 +29,11 @@ const messages = defineMessages({
requestSuccess: '<strong>{title}</strong> requested successfully!', requestSuccess: '<strong>{title}</strong> requested successfully!',
requesttitle: 'Request {title}', requesttitle: 'Request {title}',
request4ktitle: 'Request {title} in 4K', request4ktitle: 'Request {title} in 4K',
edit: 'Edit Request',
cancel: 'Cancel Request',
pendingrequest: 'Pending Request for {title}',
pending4krequest: 'Pending 4K Request for {title}',
requestfrom: "{username}'s request is pending approval.",
requestseasons: requestseasons:
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}', 'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}',
requestall: 'Request All Seasons', requestall: 'Request All Seasons',
@ -43,6 +48,7 @@ const messages = defineMessages({
requestcancelled: 'Request for <strong>{title}</strong> canceled.', requestcancelled: 'Request for <strong>{title}</strong> canceled.',
autoapproval: 'Automatic Approval', autoapproval: 'Automatic Approval',
requesterror: 'Something went wrong while submitting the request.', requesterror: 'Something went wrong while submitting the request.',
pendingapproval: 'Your request is pending approval.',
}); });
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> { interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
@ -342,6 +348,8 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
return seasonRequest; return seasonRequest;
}; };
const isOwner = editRequest && editRequest.requestedBy.id === user?.id;
return !data?.externalIds.tvdbId && searchModal.show ? ( return !data?.externalIds.tvdbId && searchModal.show ? (
<SearchByNameModal <SearchByNameModal
tvdbId={tvdbId} tvdbId={tvdbId}
@ -362,12 +370,20 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
onCancel={tvdbId ? () => setSearchModal({ show: true }) : onCancel} onCancel={tvdbId ? () => setSearchModal({ show: true }) : onCancel}
onOk={() => (editRequest ? updateRequest() : sendRequest())} onOk={() => (editRequest ? updateRequest() : sendRequest())}
title={intl.formatMessage( title={intl.formatMessage(
is4k ? messages.request4ktitle : messages.requesttitle, editRequest
? is4k
? messages.pending4krequest
: messages.pendingrequest
: is4k
? messages.request4ktitle
: messages.requesttitle,
{ title: data?.name } { title: data?.name }
)} )}
okText={ okText={
editRequest && selectedSeasons.length === 0 editRequest
? 'Cancel Request' ? selectedSeasons.length === 0
? intl.formatMessage(messages.cancel)
: intl.formatMessage(messages.edit)
: getAllRequestedSeasons().length >= getAllSeasons().length : getAllRequestedSeasons().length >= getAllSeasons().length
? intl.formatMessage(messages.alreadyrequested) ? intl.formatMessage(messages.alreadyrequested)
: !settings.currentSettings.partialRequestsEnabled : !settings.currentSettings.partialRequestsEnabled
@ -397,12 +413,21 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
: `primary` : `primary`
} }
cancelText={ cancelText={
tvdbId editRequest
? intl.formatMessage(globalMessages.close)
: tvdbId
? intl.formatMessage(globalMessages.back) ? intl.formatMessage(globalMessages.back)
: intl.formatMessage(globalMessages.cancel) : intl.formatMessage(globalMessages.cancel)
} }
iconSvg={<DownloadIcon className="w-6 h-6" />} iconSvg={<DownloadIcon className="w-6 h-6" />}
> >
{editRequest
? isOwner
? intl.formatMessage(messages.pendingapproval)
: intl.formatMessage(messages.requestfrom, {
username: editRequest?.requestedBy.displayName,
})
: null}
{hasPermission( {hasPermission(
[ [
Permission.MANAGE_REQUESTS, Permission.MANAGE_REQUESTS,

@ -63,7 +63,7 @@ const messages = defineMessages({
'Password is too short; should be a minimum of 8 characters', 'Password is too short; should be a minimum of 8 characters',
usercreatedfailed: 'Something went wrong while creating the user.', usercreatedfailed: 'Something went wrong while creating the user.',
usercreatedfailedexisting: usercreatedfailedexisting:
'Provided email is already in use by another user.', 'The provided email address is already in use by another user.',
usercreatedsuccess: 'User created successfully!', usercreatedsuccess: 'User created successfully!',
email: 'Email Address', email: 'Email Address',
password: 'Password', password: 'Password',

@ -9,7 +9,7 @@ const globalMessages = defineMessages({
requested: 'Requested', requested: 'Requested',
requesting: 'Requesting…', requesting: 'Requesting…',
request: 'Request', request: 'Request',
request4k: 'Request 4K', request4k: 'Request in 4K',
failed: 'Failed', failed: 'Failed',
pending: 'Pending', pending: 'Pending',
declined: 'Declined', declined: 'Declined',

@ -52,7 +52,7 @@
"components.Login.loginerror": "Something went wrong while trying to sign in.", "components.Login.loginerror": "Something went wrong while trying to sign in.",
"components.Login.password": "Password", "components.Login.password": "Password",
"components.Login.signin": "Sign In", "components.Login.signin": "Sign In",
"components.Login.signingin": "Signing in…", "components.Login.signingin": "Signing In…",
"components.Login.signinheader": "Sign in to continue", "components.Login.signinheader": "Sign in to continue",
"components.Login.signinwithoverseerr": "Use your {applicationTitle} account", "components.Login.signinwithoverseerr": "Use your {applicationTitle} account",
"components.Login.signinwithplex": "Use your Plex account", "components.Login.signinwithplex": "Use your Plex account",
@ -140,7 +140,7 @@
"components.PersonDetails.birthdate": "Born {birthdate}", "components.PersonDetails.birthdate": "Born {birthdate}",
"components.PersonDetails.crewmember": "Crew", "components.PersonDetails.crewmember": "Crew",
"components.PersonDetails.lifespan": "{birthdate} {deathdate}", "components.PersonDetails.lifespan": "{birthdate} {deathdate}",
"components.PlexLoginButton.signingin": "Signing in…", "components.PlexLoginButton.signingin": "Signing In…",
"components.PlexLoginButton.signinwithplex": "Sign In", "components.PlexLoginButton.signinwithplex": "Sign In",
"components.QuotaSelector.movieRequestLimit": "{quotaLimit} movie(s) per {quotaDays} day(s)", "components.QuotaSelector.movieRequestLimit": "{quotaLimit} movie(s) per {quotaDays} day(s)",
"components.QuotaSelector.tvRequestLimit": "{quotaLimit} season(s) per {quotaDays} day(s)", "components.QuotaSelector.tvRequestLimit": "{quotaLimit} season(s) per {quotaDays} day(s)",
@ -152,16 +152,16 @@
"components.RequestBlock.rootfolder": "Root Folder", "components.RequestBlock.rootfolder": "Root Folder",
"components.RequestBlock.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestBlock.seasons": "{seasonCount, plural, one {Season} other {Seasons}}",
"components.RequestBlock.server": "Destination Server", "components.RequestBlock.server": "Destination Server",
"components.RequestButton.approve4krequests": "Approve {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}", "components.RequestButton.approve4krequests": "Approve {requestCount, plural, one {Request} other {{requestCount} 4K Requests}}",
"components.RequestButton.approverequest": "Approve Request", "components.RequestButton.approverequest": "Approve Request",
"components.RequestButton.approverequest4k": "Approve 4K Request", "components.RequestButton.approverequest4k": "Approve 4K Request",
"components.RequestButton.approverequests": "Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}", "components.RequestButton.approverequests": "Approve {requestCount, plural, one {Request} other {{requestCount} Requests}}",
"components.RequestButton.decline4krequests": "Decline {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}", "components.RequestButton.decline4krequests": "Decline {requestCount, plural, one {Request} other {{requestCount} 4K Requests}}",
"components.RequestButton.declinerequest": "Decline Request", "components.RequestButton.declinerequest": "Decline Request",
"components.RequestButton.declinerequest4k": "Decline 4K Request", "components.RequestButton.declinerequest4k": "Decline 4K Request",
"components.RequestButton.declinerequests": "Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}", "components.RequestButton.declinerequests": "Decline {requestCount, plural, one {Request} other {{requestCount} Requests}}",
"components.RequestButton.requestmore": "Request More", "components.RequestButton.requestmore": "Request More",
"components.RequestButton.requestmore4k": "Request More 4K", "components.RequestButton.requestmore4k": "Request More in 4K",
"components.RequestButton.viewrequest": "View Request", "components.RequestButton.viewrequest": "View Request",
"components.RequestButton.viewrequest4k": "View 4K Request", "components.RequestButton.viewrequest4k": "View 4K Request",
"components.RequestCard.deleterequest": "Delete Request", "components.RequestCard.deleterequest": "Delete Request",
@ -169,6 +169,7 @@
"components.RequestCard.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestCard.seasons": "{seasonCount, plural, one {Season} other {Seasons}}",
"components.RequestList.RequestItem.cancelRequest": "Cancel Request", "components.RequestList.RequestItem.cancelRequest": "Cancel Request",
"components.RequestList.RequestItem.deleterequest": "Delete Request", "components.RequestList.RequestItem.deleterequest": "Delete Request",
"components.RequestList.RequestItem.editrequest": "Edit Request",
"components.RequestList.RequestItem.failedretry": "Something went wrong while retrying the request.", "components.RequestList.RequestItem.failedretry": "Something went wrong while retrying the request.",
"components.RequestList.RequestItem.mediaerror": "The associated title for this request is no longer available.", "components.RequestList.RequestItem.mediaerror": "The associated title for this request is no longer available.",
"components.RequestList.RequestItem.modified": "Modified", "components.RequestList.RequestItem.modified": "Modified",
@ -208,13 +209,13 @@
"components.RequestModal.alreadyrequested": "Already Requested", "components.RequestModal.alreadyrequested": "Already Requested",
"components.RequestModal.autoapproval": "Automatic Approval", "components.RequestModal.autoapproval": "Automatic Approval",
"components.RequestModal.cancel": "Cancel Request", "components.RequestModal.cancel": "Cancel Request",
"components.RequestModal.edit": "Edit Request",
"components.RequestModal.errorediting": "Something went wrong while editing the request.", "components.RequestModal.errorediting": "Something went wrong while editing the request.",
"components.RequestModal.extras": "Extras", "components.RequestModal.extras": "Extras",
"components.RequestModal.numberofepisodes": "# of Episodes", "components.RequestModal.numberofepisodes": "# of Episodes",
"components.RequestModal.pending4krequest": "Pending Request for {title} in 4K", "components.RequestModal.pending4krequest": "Pending 4K Request for {title}",
"components.RequestModal.pendingapproval": "Your request is pending approval.", "components.RequestModal.pendingapproval": "Your request is pending approval.",
"components.RequestModal.pendingrequest": "Pending Request for {title}", "components.RequestModal.pendingrequest": "Pending Request for {title}",
"components.RequestModal.request4kfrom": "There is currently a pending 4K request from {username}.",
"components.RequestModal.request4ktitle": "Request {title} in 4K", "components.RequestModal.request4ktitle": "Request {title} in 4K",
"components.RequestModal.requestCancel": "Request for <strong>{title}</strong> canceled.", "components.RequestModal.requestCancel": "Request for <strong>{title}</strong> canceled.",
"components.RequestModal.requestSuccess": "<strong>{title}</strong> requested successfully!", "components.RequestModal.requestSuccess": "<strong>{title}</strong> requested successfully!",
@ -223,7 +224,7 @@
"components.RequestModal.requestcancelled": "Request for <strong>{title}</strong> canceled.", "components.RequestModal.requestcancelled": "Request for <strong>{title}</strong> canceled.",
"components.RequestModal.requestedited": "Request for <strong>{title}</strong> edited successfully!", "components.RequestModal.requestedited": "Request for <strong>{title}</strong> edited successfully!",
"components.RequestModal.requesterror": "Something went wrong while submitting the request.", "components.RequestModal.requesterror": "Something went wrong while submitting the request.",
"components.RequestModal.requestfrom": "There is currently a pending request from {username}.", "components.RequestModal.requestfrom": "{username}'s request is pending approval.",
"components.RequestModal.requestseasons": "Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}", "components.RequestModal.requestseasons": "Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}",
"components.RequestModal.requesttitle": "Request {title}", "components.RequestModal.requesttitle": "Request {title}",
"components.RequestModal.season": "Season", "components.RequestModal.season": "Season",
@ -677,6 +678,7 @@
"components.UserList.totalrequests": "Total Requests", "components.UserList.totalrequests": "Total Requests",
"components.UserList.user": "User", "components.UserList.user": "User",
"components.UserList.usercreatedfailed": "Something went wrong while creating the user.", "components.UserList.usercreatedfailed": "Something went wrong while creating the user.",
"components.UserList.usercreatedfailedexisting": "The provided email address is already in use by another user.",
"components.UserList.usercreatedsuccess": "User created successfully!", "components.UserList.usercreatedsuccess": "User created successfully!",
"components.UserList.userdeleted": "User deleted successfully!", "components.UserList.userdeleted": "User deleted successfully!",
"components.UserList.userdeleteerror": "Something went wrong while deleting the user.", "components.UserList.userdeleteerror": "Something went wrong while deleting the user.",
@ -796,7 +798,7 @@
"i18n.previous": "Previous", "i18n.previous": "Previous",
"i18n.processing": "Processing", "i18n.processing": "Processing",
"i18n.request": "Request", "i18n.request": "Request",
"i18n.request4k": "Request 4K", "i18n.request4k": "Request in 4K",
"i18n.requested": "Requested", "i18n.requested": "Requested",
"i18n.requesting": "Requesting…", "i18n.requesting": "Requesting…",
"i18n.resultsperpage": "Display {pageSize} results per page", "i18n.resultsperpage": "Display {pageSize} results per page",

Loading…
Cancel
Save