From 89455ad9b783d04d993a0009c351b1096f2b222e Mon Sep 17 00:00:00 2001 From: TheCatLady <52870424+TheCatLady@users.noreply.github.com> Date: Sun, 18 Apr 2021 23:12:05 -0400 Subject: [PATCH] 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 --- server/routes/request.ts | 34 ++-- src/components/Login/LocalLogin.tsx | 2 +- src/components/PlexLoginButton/index.tsx | 2 +- src/components/RequestButton/index.tsx | 146 ++++++++++-------- .../RequestList/RequestItem/index.tsx | 99 ++++++------ .../RequestModal/MovieRequestModal.tsx | 84 +++++----- .../RequestModal/TvRequestModal.tsx | 33 +++- src/components/UserList/index.tsx | 2 +- src/i18n/globalMessages.ts | 2 +- src/i18n/locale/en.json | 24 +-- 10 files changed, 246 insertions(+), 182 deletions(-) diff --git a/server/routes/request.ts b/server/routes/request.ts index 9fb572631e..df0b55453d 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -493,7 +493,6 @@ requestRoutes.get('/:requestId', async (req, res, next) => { requestRoutes.put<{ requestId: string }>( '/:requestId', - isAuthenticated(Permission.MANAGE_REQUESTS), async (req, res, next) => { const requestRepository = getRepository(MediaRequest); const userRepository = getRepository(User); @@ -503,17 +502,30 @@ requestRoutes.put<{ requestId: string }>( ); 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; if ( req.body.userId && - !( - req.user?.hasPermission(Permission.MANAGE_USERS) && - req.user?.hasPermission(Permission.MANAGE_REQUESTS) - ) + req.body.userId !== req.user?.id && + !req.user?.hasPermission([ + Permission.MANAGE_USERS, + Permission.MANAGE_REQUESTS, + ]) ) { return next({ status: 403, @@ -546,7 +558,7 @@ requestRoutes.put<{ requestId: string }>( if (!requestedSeasons || requestedSeasons.length === 0) { 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({ 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(); } catch (e) { 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', 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', message: e.message, }); - next({ status: 404, message: 'Request not found' }); + next({ status: 404, message: 'Request not found.' }); } } ); diff --git a/src/components/Login/LocalLogin.tsx b/src/components/Login/LocalLogin.tsx index 1dc6006deb..6444635f25 100644 --- a/src/components/Login/LocalLogin.tsx +++ b/src/components/Login/LocalLogin.tsx @@ -13,7 +13,7 @@ const messages = defineMessages({ validationemailrequired: 'You must provide a valid email address', validationpasswordrequired: 'You must provide a password', loginerror: 'Something went wrong while trying to sign in.', - signingin: 'Signing in…', + signingin: 'Signing In…', signin: 'Sign In', forgotpassword: 'Forgot Password?', }); diff --git a/src/components/PlexLoginButton/index.tsx b/src/components/PlexLoginButton/index.tsx index 2125f00537..c85fa78c6e 100644 --- a/src/components/PlexLoginButton/index.tsx +++ b/src/components/PlexLoginButton/index.tsx @@ -6,7 +6,7 @@ import PlexOAuth from '../../utils/plex'; const messages = defineMessages({ signinwithplex: 'Sign In', - signingin: 'Signing in…', + signingin: 'Signing In…', }); const plexOAuth = new PlexOAuth(); diff --git a/src/components/RequestButton/index.tsx b/src/components/RequestButton/index.tsx index c5fef6fd7d..75f2b6c6d1 100644 --- a/src/components/RequestButton/index.tsx +++ b/src/components/RequestButton/index.tsx @@ -5,7 +5,7 @@ import { XIcon, } from '@heroicons/react/solid'; import axios from 'axios'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { MediaRequestStatus, @@ -23,19 +23,19 @@ const messages = defineMessages({ viewrequest: 'View Request', viewrequest4k: 'View 4K Request', requestmore: 'Request More', - requestmore4k: 'Request More 4K', + requestmore4k: 'Request More in 4K', approverequest: 'Approve Request', approverequest4k: 'Approve 4K Request', declinerequest: 'Decline Request', declinerequest4k: 'Decline 4K Request', approverequests: - 'Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}', + 'Approve {requestCount, plural, one {Request} other {{requestCount} Requests}}', declinerequests: - 'Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}', + 'Decline {requestCount, plural, one {Request} other {{requestCount} Requests}}', approve4krequests: - 'Approve {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}', + 'Approve {requestCount, plural, one {Request} other {{requestCount} 4K Requests}}', decline4krequests: - 'Decline {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}', + 'Decline {requestCount, plural, one {Request} other {{requestCount} 4K Requests}}', }); interface ButtonOption { @@ -64,26 +64,34 @@ const RequestButton: React.FC = ({ }) => { const intl = useIntl(); const settings = useSettings(); - const { hasPermission } = useUser(); + const { user, hasPermission } = useUser(); const [showRequestModal, setShowRequestModal] = useState(false); const [showRequest4kModal, setShowRequest4kModal] = useState(false); + const [editRequest, setEditRequest] = useState(false); - const activeRequest = media?.requests.find( - (request) => request.status === MediaRequestStatus.PENDING && !request.is4k - ); - const active4kRequest = media?.requests.find( - (request) => request.status === MediaRequestStatus.PENDING && request.is4k - ); - - // All pending + // All pending requests const activeRequests = media?.requests.filter( (request) => request.status === MediaRequestStatus.PENDING && !request.is4k ); - const active4kRequests = media?.requests.filter( (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 ( request: MediaRequest, type: 'approve' | 'decline' @@ -121,24 +129,7 @@ const RequestButton: React.FC = ({ id: 'request', text: intl.formatMessage(globalMessages.request), action: () => { - setShowRequestModal(true); - }, - svg: , - }); - } - - 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: () => { + setEditRequest(false); setShowRequestModal(true); }, svg: , @@ -157,26 +148,7 @@ const RequestButton: React.FC = ({ id: 'request4k', text: intl.formatMessage(globalMessages.request4k), action: () => { - setShowRequest4kModal(true); - }, - svg: , - }); - } - - 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: () => { + setEditRequest(false); setShowRequest4kModal(true); }, svg: , @@ -185,27 +157,34 @@ const RequestButton: React.FC = ({ if ( activeRequest && - mediaType === 'movie' && - hasPermission(Permission.REQUEST) + (activeRequest.requestedBy.id === user?.id || + (activeRequests?.length === 1 && + hasPermission(Permission.MANAGE_REQUESTS))) ) { buttons.push({ id: 'active-request', text: intl.formatMessage(messages.viewrequest), - action: () => setShowRequestModal(true), + action: () => { + setEditRequest(true); + setShowRequestModal(true); + }, svg: , }); } if ( active4kRequest && - mediaType === 'movie' && - (hasPermission(Permission.REQUEST_4K) || - hasPermission(Permission.REQUEST_4K_MOVIE)) + (active4kRequest.requestedBy.id === user?.id || + (active4kRequests?.length === 1 && + hasPermission(Permission.MANAGE_REQUESTS))) ) { buttons.push({ id: 'active-4k-request', text: intl.formatMessage(messages.viewrequest4k), - action: () => setShowRequest4kModal(true), + action: () => { + setEditRequest(true); + setShowRequest4kModal(true); + }, svg: , }); } @@ -320,6 +299,49 @@ const RequestButton: React.FC = ({ ); } + 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: , + }); + } + + 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: , + }); + } + const [buttonOne, ...others] = buttons; if (!buttonOne) { @@ -332,6 +354,7 @@ const RequestButton: React.FC = ({ tmdbId={tmdbId} show={showRequestModal} type={mediaType} + editRequest={editRequest ? activeRequest : undefined} onComplete={() => { onUpdate(); setShowRequestModal(false); @@ -342,6 +365,7 @@ const RequestButton: React.FC = ({ tmdbId={tmdbId} show={showRequest4kModal} type={mediaType} + editRequest={editRequest ? active4kRequest : undefined} is4k onComplete={() => { onUpdate(); diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 13b7c01eeb..01fb1ddc1f 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -36,6 +36,7 @@ const messages = defineMessages({ modified: 'Modified', modifieduserdate: '{date} by {user}', mediaerror: 'The associated title for this request is no longer available.', + editrequest: 'Edit Request', deleterequest: 'Delete Request', cancelRequest: 'Cancel Request', }); @@ -363,20 +364,6 @@ const RequestItem: React.FC = ({
- {requestData.status === MediaRequestStatus.PENDING && - !hasPermission(Permission.MANAGE_REQUESTS) && - requestData.requestedBy.id === user?.id && ( - deleteRequest()} - confirmText={intl.formatMessage(globalMessages.areyousure)} - className="w-full" - > - - - {intl.formatMessage(messages.cancelRequest)} - - - )} {requestData.media[requestData.is4k ? 'status4k' : 'status'] === MediaStatus.UNKNOWN && requestData.status !== MediaRequestStatus.DECLINED && @@ -407,52 +394,70 @@ const RequestItem: React.FC = ({ > - {intl.formatMessage(globalMessages.delete)} + {intl.formatMessage(messages.deleterequest)} )} {requestData.status === MediaRequestStatus.PENDING && hasPermission(Permission.MANAGE_REQUESTS) && ( - <> -
- - - - - - -
+
- + + + +
+ )} + {requestData.status === MediaRequestStatus.PENDING && + (hasPermission(Permission.MANAGE_REQUESTS) || + (requestData.requestedBy.id === user?.id && + (requestData.type === 'tv' || + hasPermission(Permission.REQUEST_ADVANCED)))) && ( + + + + )} + {requestData.status === MediaRequestStatus.PENDING && + !hasPermission(Permission.MANAGE_REQUESTS) && + requestData.requestedBy.id === user?.id && ( + deleteRequest()} + confirmText={intl.formatMessage(globalMessages.areyousure)} + className="w-full" + > + + + {intl.formatMessage(messages.cancelRequest)} + + )}
diff --git a/src/components/RequestModal/MovieRequestModal.tsx b/src/components/RequestModal/MovieRequestModal.tsx index c150d242da..bd2512aeef 100644 --- a/src/components/RequestModal/MovieRequestModal.tsx +++ b/src/components/RequestModal/MovieRequestModal.tsx @@ -4,10 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; -import { - MediaRequestStatus, - MediaStatus, -} from '../../../server/constants/media'; +import { MediaStatus } from '../../../server/constants/media'; import { MediaRequest } from '../../../server/entity/MediaRequest'; import { QuotaResponse } from '../../../server/interfaces/api/userInterfaces'; import { Permission } from '../../../server/lib/permissions'; @@ -25,11 +22,11 @@ const messages = defineMessages({ requestCancel: 'Request for {title} canceled.', requesttitle: 'Request {title}', request4ktitle: 'Request {title} in 4K', + edit: 'Edit Request', cancel: 'Cancel Request', pendingrequest: 'Pending Request for {title}', - pending4krequest: 'Pending Request for {title} in 4K', - requestfrom: 'There is currently a pending request from {username}.', - request4kfrom: 'There is currently a pending 4K request from {username}.', + pending4krequest: 'Pending 4K Request for {title}', + requestfrom: "{username}'s request is pending approval.", errorediting: 'Something went wrong while editing the request.', requestedited: 'Request for {title} edited successfully!', requesterror: 'Something went wrong while submitting the request.', @@ -130,18 +127,14 @@ const MovieRequestModal: React.FC = ({ } finally { setIsUpdating(false); } - }, [data, onComplete, addToast, requestOverrides]); - - const activeRequest = data?.mediaInfo?.requests?.find( - (request) => request.is4k === !!is4k - ); + }, [data, onComplete, addToast, requestOverrides, hasPermission, intl, is4k]); const cancelRequest = async () => { setIsUpdating(true); try { const response = await axios.delete( - `/api/v1/request/${activeRequest?.id}` + `/api/v1/request/${editRequest?.id}` ); if (response.status === 204) { @@ -206,11 +199,15 @@ const MovieRequestModal: React.FC = ({ } }; - const isOwner = activeRequest - ? activeRequest.requestedBy.id === user?.id - : false; + if (editRequest) { + const isOwner = editRequest.requestedBy.id === user?.id; + const showEditButton = hasPermission( + [Permission.MANAGE_REQUESTS, Permission.REQUEST_ADVANCED], + { + type: 'or', + } + ); - if (activeRequest?.status === MediaRequestStatus.PENDING) { return ( = ({ onCancel={onCancel} title={intl.formatMessage( is4k ? messages.pending4krequest : messages.pendingrequest, - { - title: data?.title, - } + { title: data?.title } )} - onOk={() => (isOwner ? cancelRequest() : updateRequest())} + onOk={() => (showEditButton ? updateRequest() : cancelRequest())} okDisabled={isUpdating} okText={ - isOwner - ? isUpdating - ? intl.formatMessage(globalMessages.canceling) - : intl.formatMessage(messages.cancel) - : intl.formatMessage(globalMessages.edit) + showEditButton + ? intl.formatMessage(messages.edit) + : intl.formatMessage(messages.cancel) } - 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)} iconSvg={} > {isOwner ? intl.formatMessage(messages.pendingapproval) - : intl.formatMessage( - is4k ? messages.request4kfrom : messages.requestfrom, - { - username: activeRequest.requestedBy.displayName, - } - )} + : intl.formatMessage(messages.requestfrom, { + username: editRequest.requestedBy.displayName, + })} {(hasPermission(Permission.REQUEST_ADVANCED) || hasPermission(Permission.MANAGE_REQUESTS)) && (
{ setRequestOverrides(overrides); }} diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 69b9da1a44..215a7abc78 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -29,6 +29,11 @@ const messages = defineMessages({ requestSuccess: '{title} requested successfully!', requesttitle: 'Request {title}', 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: 'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}', requestall: 'Request All Seasons', @@ -43,6 +48,7 @@ const messages = defineMessages({ requestcancelled: 'Request for {title} canceled.', autoapproval: 'Automatic Approval', requesterror: 'Something went wrong while submitting the request.', + pendingapproval: 'Your request is pending approval.', }); interface RequestModalProps extends React.HTMLAttributes { @@ -342,6 +348,8 @@ const TvRequestModal: React.FC = ({ return seasonRequest; }; + const isOwner = editRequest && editRequest.requestedBy.id === user?.id; + return !data?.externalIds.tvdbId && searchModal.show ? ( = ({ onCancel={tvdbId ? () => setSearchModal({ show: true }) : onCancel} onOk={() => (editRequest ? updateRequest() : sendRequest())} title={intl.formatMessage( - is4k ? messages.request4ktitle : messages.requesttitle, + editRequest + ? is4k + ? messages.pending4krequest + : messages.pendingrequest + : is4k + ? messages.request4ktitle + : messages.requesttitle, { title: data?.name } )} okText={ - editRequest && selectedSeasons.length === 0 - ? 'Cancel Request' + editRequest + ? selectedSeasons.length === 0 + ? intl.formatMessage(messages.cancel) + : intl.formatMessage(messages.edit) : getAllRequestedSeasons().length >= getAllSeasons().length ? intl.formatMessage(messages.alreadyrequested) : !settings.currentSettings.partialRequestsEnabled @@ -397,12 +413,21 @@ const TvRequestModal: React.FC = ({ : `primary` } cancelText={ - tvdbId + editRequest + ? intl.formatMessage(globalMessages.close) + : tvdbId ? intl.formatMessage(globalMessages.back) : intl.formatMessage(globalMessages.cancel) } iconSvg={} > + {editRequest + ? isOwner + ? intl.formatMessage(messages.pendingapproval) + : intl.formatMessage(messages.requestfrom, { + username: editRequest?.requestedBy.displayName, + }) + : null} {hasPermission( [ Permission.MANAGE_REQUESTS, diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 95516d488c..a0a6675828 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -63,7 +63,7 @@ const messages = defineMessages({ 'Password is too short; should be a minimum of 8 characters', usercreatedfailed: 'Something went wrong while creating the user.', 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!', email: 'Email Address', password: 'Password', diff --git a/src/i18n/globalMessages.ts b/src/i18n/globalMessages.ts index 58d260e74d..4c0ef7905c 100644 --- a/src/i18n/globalMessages.ts +++ b/src/i18n/globalMessages.ts @@ -9,7 +9,7 @@ const globalMessages = defineMessages({ requested: 'Requested', requesting: 'Requesting…', request: 'Request', - request4k: 'Request 4K', + request4k: 'Request in 4K', failed: 'Failed', pending: 'Pending', declined: 'Declined', diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index bab294b3f4..2a7725c82f 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -52,7 +52,7 @@ "components.Login.loginerror": "Something went wrong while trying to sign in.", "components.Login.password": "Password", "components.Login.signin": "Sign In", - "components.Login.signingin": "Signing in…", + "components.Login.signingin": "Signing In…", "components.Login.signinheader": "Sign in to continue", "components.Login.signinwithoverseerr": "Use your {applicationTitle} account", "components.Login.signinwithplex": "Use your Plex account", @@ -140,7 +140,7 @@ "components.PersonDetails.birthdate": "Born {birthdate}", "components.PersonDetails.crewmember": "Crew", "components.PersonDetails.lifespan": "{birthdate} – {deathdate}", - "components.PlexLoginButton.signingin": "Signing in…", + "components.PlexLoginButton.signingin": "Signing In…", "components.PlexLoginButton.signinwithplex": "Sign In", "components.QuotaSelector.movieRequestLimit": "{quotaLimit} movie(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.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "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.approverequest4k": "Approve 4K Request", - "components.RequestButton.approverequests": "Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}", - "components.RequestButton.decline4krequests": "Decline {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}", + "components.RequestButton.approverequests": "Approve {requestCount, plural, one {Request} other {{requestCount} Requests}}", + "components.RequestButton.decline4krequests": "Decline {requestCount, plural, one {Request} other {{requestCount} 4K Requests}}", "components.RequestButton.declinerequest": "Decline 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.requestmore4k": "Request More 4K", + "components.RequestButton.requestmore4k": "Request More in 4K", "components.RequestButton.viewrequest": "View Request", "components.RequestButton.viewrequest4k": "View 4K Request", "components.RequestCard.deleterequest": "Delete Request", @@ -169,6 +169,7 @@ "components.RequestCard.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestList.RequestItem.cancelRequest": "Cancel 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.mediaerror": "The associated title for this request is no longer available.", "components.RequestList.RequestItem.modified": "Modified", @@ -208,13 +209,13 @@ "components.RequestModal.alreadyrequested": "Already Requested", "components.RequestModal.autoapproval": "Automatic Approval", "components.RequestModal.cancel": "Cancel Request", + "components.RequestModal.edit": "Edit Request", "components.RequestModal.errorediting": "Something went wrong while editing the request.", "components.RequestModal.extras": "Extras", "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.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.requestCancel": "Request for {title} canceled.", "components.RequestModal.requestSuccess": "{title} requested successfully!", @@ -223,7 +224,7 @@ "components.RequestModal.requestcancelled": "Request for {title} canceled.", "components.RequestModal.requestedited": "Request for {title} edited successfully!", "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.requesttitle": "Request {title}", "components.RequestModal.season": "Season", @@ -677,6 +678,7 @@ "components.UserList.totalrequests": "Total Requests", "components.UserList.user": "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.userdeleted": "User deleted successfully!", "components.UserList.userdeleteerror": "Something went wrong while deleting the user.", @@ -796,7 +798,7 @@ "i18n.previous": "Previous", "i18n.processing": "Processing", "i18n.request": "Request", - "i18n.request4k": "Request 4K", + "i18n.request4k": "Request in 4K", "i18n.requested": "Requested", "i18n.requesting": "Requesting…", "i18n.resultsperpage": "Display {pageSize} results per page",