diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index 13d6119a2..b65df670c 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -1,38 +1,24 @@ -import { - CheckIcon, - PencilIcon, - RefreshIcon, - TrashIcon, - XIcon, -} from '@heroicons/react/solid'; -import axios from 'axios'; -import Link from 'next/link'; -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect } from 'react'; import { useInView } from 'react-intersection-observer'; -import { defineMessages, useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR, { mutate } from 'swr'; -import { - MediaRequestStatus, - MediaStatus, -} from '../../../server/constants/media'; import type { MediaRequest } from '../../../server/entity/MediaRequest'; -import type { MovieDetails } from '../../../server/models/Movie'; import type { TvDetails } from '../../../server/models/Tv'; -import { Permission, useUser } from '../../hooks/useUser'; -import globalMessages from '../../i18n/globalMessages'; -import { withProperties } from '../../utils/typeHelpers'; +import type { MovieDetails } from '../../../server/models/Movie'; +import useSWR from 'swr'; +import { LanguageContext } from '../../context/LanguageContext'; +import { MediaRequestStatus } from '../../../server/constants/media'; import Badge from '../Common/Badge'; +import { useUser, Permission } from '../../hooks/useUser'; +import axios from 'axios'; import Button from '../Common/Button'; -import CachedImage from '../Common/CachedImage'; -import RequestModal from '../RequestModal'; +import { withProperties } from '../../utils/typeHelpers'; +import Link from 'next/link'; +import { defineMessages, useIntl } from 'react-intl'; +import globalMessages from '../../i18n/globalMessages'; import StatusBadge from '../StatusBadge'; const messages = defineMessages({ - seasons: '{seasonCount, plural, one {Season} other {Seasons}}', - failedretry: 'Something went wrong while retrying the request.', - mediaerror: 'The associated title for this request is no longer available.', - deleterequest: 'Delete Request', + seasons: 'Seasons', + all: 'All', }); const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { @@ -41,7 +27,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { const RequestCardPlaceholder: React.FC = () => { return ( -
+
@@ -49,45 +35,6 @@ const RequestCardPlaceholder: React.FC = () => { ); }; -interface RequestCardErrorProps { - mediaId?: number; -} - -const RequestCardError: React.FC = ({ mediaId }) => { - const { hasPermission } = useUser(); - const intl = useIntl(); - - const deleteRequest = async () => { - await axios.delete(`/api/v1/media/${mediaId}`); - mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0'); - }; - - return ( -
-
-
-
-
- {intl.formatMessage(messages.mediaerror)} -
- {hasPermission(Permission.MANAGE_REQUESTS) && mediaId && ( - - )} -
-
-
-
- ); -}; - interface RequestCardProps { request: MediaRequest; onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void; @@ -98,16 +45,14 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { triggerOnce: true, }); const intl = useIntl(); - const { user, hasPermission } = useUser(); - const { addToast } = useToasts(); - const [isRetrying, setRetrying] = useState(false); - const [showEditModal, setShowEditModal] = useState(false); + const { hasPermission } = useUser(); + const { locale } = useContext(LanguageContext); const url = request.type === 'movie' ? `/api/v1/movie/${request.media.tmdbId}` : `/api/v1/tv/${request.media.tmdbId}`; const { data: title, error } = useSWR( - inView ? `${url}` : null + inView ? `${url}?language=${locale}` : null ); const { data: requestData, @@ -125,30 +70,6 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { } }; - const deleteRequest = async () => { - await axios.delete(`/api/v1/request/${request.id}`); - mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0'); - }; - - const retryRequest = async () => { - setRetrying(true); - - try { - const response = await axios.post(`/api/v1/request/${request.id}/retry`); - - if (response) { - revalidate(); - } - } catch (e) { - addToast(intl.formatMessage(messages.failedretry), { - autoDismiss: true, - appearance: 'error', - }); - } finally { - setRetrying(false); - } - }; - useEffect(() => { if (title && onTitleData) { onTitleData(request.id, title); @@ -164,242 +85,157 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { } if (!requestData && !requestError) { - return ; + return ; } if (!title || !requestData) { - return ; + return ; } return ( - <> - setShowEditModal(false)} - onComplete={() => { - revalidate(); - setShowEditModal(false); - }} - /> -
- {title.backdropPath && ( -
- +
+

+ + {isMovie(title) ? title.title : title.name} + +

+ + + -
+ {requestData.requestedBy.displayName} + + + + {requestData.media.status && ( +
+ 0 + } />
)} -
-
- {(isMovie(title) ? title.releaseDate : title.firstAirDate)?.slice( - 0, - 4 - )} -
- - - {isMovie(title) ? title.title : title.name} - - - {hasPermission( - [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], - { type: 'or' } - ) && ( - - )} - {!isMovie(title) && request.seasons.length > 0 && ( -
- - {intl.formatMessage(messages.seasons, { - seasonCount: - title.seasons.filter((season) => season.seasonNumber !== 0) - .length === request.seasons.length - ? 0 - : request.seasons.length, - })} + {request.seasons.length > 0 && ( +
+ {intl.formatMessage(messages.seasons)} + {!isMovie(title) && + title.seasons.filter((season) => season.seasonNumber !== 0) + .length === request.seasons.length ? ( + + {intl.formatMessage(messages.all)} - {title.seasons.filter((season) => season.seasonNumber !== 0) - .length === request.seasons.length ? ( - - {intl.formatMessage(globalMessages.all)} - - ) : ( -
- {request.seasons.map((season) => ( - - {season.seasonNumber} - - ))} -
- )} -
- )} -
- - {intl.formatMessage(globalMessages.status)} - - {requestData.media[requestData.is4k ? 'status4k' : 'status'] === - MediaStatus.UNKNOWN || - requestData.status === MediaRequestStatus.DECLINED ? ( - - {requestData.status === MediaRequestStatus.DECLINED - ? intl.formatMessage(globalMessages.declined) - : intl.formatMessage(globalMessages.failed)} - ) : ( - 0 - } - is4k={requestData.is4k} - plexUrl={requestData.media.plexUrl} - plexUrl4k={requestData.media.plexUrl4k} - /> +
+ {request.seasons.map((season) => ( + + {season.seasonNumber} + + ))} +
)}
-
- {requestData.media[requestData.is4k ? 'status4k' : 'status'] === - MediaStatus.UNKNOWN && - requestData.status !== MediaRequestStatus.DECLINED && - hasPermission(Permission.MANAGE_REQUESTS) && ( + )} + {requestData.status === MediaRequestStatus.PENDING && + hasPermission(Permission.MANAGE_REQUESTS) && ( +
+ - )} - {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 && ( + + - )} -
-
+
+
+ )} +
+
- - - +
- +
); }; diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index cfef2c739..59cdea66d 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -1,88 +1,41 @@ -import { - CheckIcon, - PencilIcon, - RefreshIcon, - TrashIcon, - XIcon, -} from '@heroicons/react/solid'; -import axios from 'axios'; -import Link from 'next/link'; -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { useInView } from 'react-intersection-observer'; -import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; +import type { MediaRequest } from '../../../../server/entity/MediaRequest'; +import { + useIntl, + FormattedDate, + FormattedRelativeTime, + defineMessages, +} from 'react-intl'; +import { useUser, Permission } from '../../../hooks/useUser'; +import { LanguageContext } from '../../../context/LanguageContext'; +import type { MovieDetails } from '../../../../server/models/Movie'; +import type { TvDetails } from '../../../../server/models/Tv'; import useSWR from 'swr'; +import Badge from '../../Common/Badge'; +import StatusBadge from '../../StatusBadge'; +import Table from '../../Common/Table'; import { MediaRequestStatus, MediaStatus, } from '../../../../server/constants/media'; -import type { MediaRequest } from '../../../../server/entity/MediaRequest'; -import type { MovieDetails } from '../../../../server/models/Movie'; -import type { TvDetails } from '../../../../server/models/Tv'; -import { Permission, useUser } from '../../../hooks/useUser'; -import globalMessages from '../../../i18n/globalMessages'; -import Badge from '../../Common/Badge'; import Button from '../../Common/Button'; -import CachedImage from '../../Common/CachedImage'; -import ConfirmButton from '../../Common/ConfirmButton'; +import axios from 'axios'; +import globalMessages from '../../../i18n/globalMessages'; +import Link from 'next/link'; +import { useToasts } from 'react-toast-notifications'; import RequestModal from '../../RequestModal'; -import StatusBadge from '../../StatusBadge'; const messages = defineMessages({ - seasons: '{seasonCount, plural, one {Season} other {Seasons}}', + seasons: 'Seasons', + notavailable: 'N/A', failedretry: 'Something went wrong while retrying the request.', - requested: 'Requested', - requesteddate: 'Requested', - 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', }); const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { return (movie as MovieDetails).title !== undefined; }; -interface RequestItemErroProps { - mediaId?: number; - revalidateList: () => void; -} - -const RequestItemError: React.FC = ({ - mediaId, - revalidateList, -}) => { - const intl = useIntl(); - const { hasPermission } = useUser(); - - const deleteRequest = async () => { - await axios.delete(`/api/v1/media/${mediaId}`); - revalidateList(); - }; - - return ( -
- - {intl.formatMessage(messages.mediaerror)} - - {hasPermission(Permission.MANAGE_REQUESTS) && mediaId && ( -
- -
- )} -
- ); -}; - interface RequestItemProps { request: MediaRequest; revalidateList: () => void; @@ -97,14 +50,15 @@ const RequestItem: React.FC = ({ }); const { addToast } = useToasts(); const intl = useIntl(); - const { user, hasPermission } = useUser(); + const { hasPermission } = useUser(); const [showEditModal, setShowEditModal] = useState(false); + const { locale } = useContext(LanguageContext); const url = request.type === 'movie' ? `/api/v1/movie/${request.media.tmdbId}` : `/api/v1/tv/${request.media.tmdbId}`; const { data: title, error } = useSWR( - inView ? `${url}` : null + inView ? `${url}?language=${locale}` : null ); const { data: requestData, revalidate, mutate } = useSWR( `/api/v1/request/${request.id}`, @@ -147,24 +101,22 @@ const RequestItem: React.FC = ({ if (!title && !error) { return ( -
+ + + ); } if (!title || !requestData) { return ( - + + + ); } return ( - <> + = ({ setShowEditModal(false); }} /> -
- {title.backdropPath && ( -
- -
-
- )} -
-
+ +
+ + + + + +
= ({ : `/tv/${requestData.media.tmdbId}` } > - - + + {isMovie(title) ? title.title : title.name} -
-
- {(isMovie(title) - ? title.releaseDate - : title.firstAirDate - )?.slice(0, 4)} -
- - - {isMovie(title) ? title.title : title.name} - - - {!isMovie(title) && request.seasons.length > 0 && ( -
- - {intl.formatMessage(messages.seasons, { - seasonCount: - title.seasons.filter( - (season) => season.seasonNumber !== 0 - ).length === request.seasons.length - ? 0 - : request.seasons.length, - })} - - {title.seasons.filter((season) => season.seasonNumber !== 0) - .length === request.seasons.length ? ( - - {intl.formatMessage(globalMessages.all)} - - ) : ( -
- {request.seasons.map((season) => ( - - {season.seasonNumber} - - ))} -
- )} -
- )} -
-
-
-
- - {intl.formatMessage(globalMessages.status)} - - {requestData.media[requestData.is4k ? 'status4k' : 'status'] === - MediaStatus.UNKNOWN || - requestData.status === MediaRequestStatus.DECLINED ? ( - - {requestData.status === MediaRequestStatus.DECLINED - ? intl.formatMessage(globalMessages.declined) - : intl.formatMessage(globalMessages.failed)} - - ) : ( - 0 - } - is4k={requestData.is4k} - plexUrl={requestData.media.plexUrl} - plexUrl4k={requestData.media.plexUrl4k} + + + - )} -
- - {requestData.modifiedBy && ( -
- - {intl.formatMessage(messages.modified)} + + {requestData.requestedBy.displayName} - - {intl.formatMessage(messages.modifieduserdate, { - date: ( - - ), - user: ( - - - - - {requestData.modifiedBy.displayName} - - - - ), - })} + + + {requestData.seasons.length > 0 && ( +
+ + {intl.formatMessage(messages.seasons)} + {requestData.seasons.map((season) => ( + + {season.seasonNumber} + + ))}
)}
-
- {requestData.media[requestData.is4k ? 'status4k' : 'status'] === - MediaStatus.UNKNOWN && - requestData.status !== MediaRequestStatus.DECLINED && - hasPermission(Permission.MANAGE_REQUESTS) && ( - - )} - {requestData.status !== MediaRequestStatus.PENDING && - hasPermission(Permission.MANAGE_REQUESTS) && ( - deleteRequest()} - confirmText={intl.formatMessage(globalMessages.areyousure)} - className="w-full" - > - - {intl.formatMessage(messages.deleterequest)} - - )} - {requestData.status === MediaRequestStatus.PENDING && - hasPermission(Permission.MANAGE_REQUESTS) && ( -
- - - - - + + {requestData.modifiedBy.displayName} ( + + )
- )} - {requestData.status === MediaRequestStatus.PENDING && - (hasPermission(Permission.MANAGE_REQUESTS) || - (requestData.requestedBy.id === user?.id && - (requestData.type === 'tv' || - hasPermission(Permission.REQUEST_ADVANCED)))) && ( - + + ) : ( + N/A + )} +
+ + + {requestData.media[requestData.is4k ? 'status4k' : 'status'] === + MediaStatus.UNKNOWN && + requestData.status !== MediaRequestStatus.DECLINED && + hasPermission(Permission.MANAGE_REQUESTS) && ( + + )} + {requestData.status !== MediaRequestStatus.PENDING && + hasPermission(Permission.MANAGE_REQUESTS) && ( + + )} + {requestData.status === MediaRequestStatus.PENDING && + hasPermission(Permission.MANAGE_REQUESTS) && ( + <> + - )} - {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/RequestList/index.tsx b/src/components/RequestList/index.tsx index c74382282..0be3bb008 100644 --- a/src/components/RequestList/index.tsx +++ b/src/components/RequestList/index.tsx @@ -1,94 +1,51 @@ -import { - ChevronLeftIcon, - ChevronRightIcon, - FilterIcon, - SortDescendingIcon, -} from '@heroicons/react/solid'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; -import React, { useEffect, useState } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import React, { useState } from 'react'; import useSWR from 'swr'; import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces'; -import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams'; -import { useUser } from '../../hooks/useUser'; -import globalMessages from '../../i18n/globalMessages'; -import Button from '../Common/Button'; -import Header from '../Common/Header'; import LoadingSpinner from '../Common/LoadingSpinner'; -import PageTitle from '../Common/PageTitle'; import RequestItem from './RequestItem'; +import Header from '../Common/Header'; +import Table from '../Common/Table'; +import Button from '../Common/Button'; +import { defineMessages, useIntl } from 'react-intl'; +import PageTitle from '../Common/PageTitle'; const messages = defineMessages({ requests: 'Requests', + mediaInfo: 'Media Info', + status: 'Status', + requestedAt: 'Requested At', + modifiedBy: 'Last Modified By', + showingresults: + 'Showing {from} to {to} of {total} results', + resultsperpage: 'Display {pageSize} results per page', + next: 'Next', + previous: 'Previous', + filterAll: 'All', + filterPending: 'Pending', + filterApproved: 'Approved', + filterAvailable: 'Available', + filterProcessing: 'Processing', + noresults: 'No results.', showallrequests: 'Show All Requests', sortAdded: 'Request Date', sortModified: 'Last Modified', }); -enum Filter { - ALL = 'all', - PENDING = 'pending', - APPROVED = 'approved', - PROCESSING = 'processing', - AVAILABLE = 'available', - UNAVAILABLE = 'unavailable', -} - +type Filter = 'all' | 'pending' | 'approved' | 'processing' | 'available'; type Sort = 'added' | 'modified'; const RequestList: React.FC = () => { - const router = useRouter(); const intl = useIntl(); - const { user } = useUser({ - id: Number(router.query.userId), - }); - const [currentFilter, setCurrentFilter] = useState(Filter.PENDING); + const [pageIndex, setPageIndex] = useState(0); + const [currentFilter, setCurrentFilter] = useState('pending'); const [currentSort, setCurrentSort] = useState('added'); const [currentPageSize, setCurrentPageSize] = useState(10); - const page = router.query.page ? Number(router.query.page) : 1; - const pageIndex = page - 1; - const updateQueryParams = useUpdateQueryParams({ page: page.toString() }); - const { data, error, revalidate } = useSWR( `/api/v1/request?take=${currentPageSize}&skip=${ pageIndex * currentPageSize - }&filter=${currentFilter}&sort=${currentSort}${ - router.query.userId ? `&requestedBy=${router.query.userId}` : '' - }` + }&filter=${currentFilter}&sort=${currentSort}` ); - - // Restore last set filter values on component mount - useEffect(() => { - const filterString = window.localStorage.getItem('rl-filter-settings'); - - if (filterString) { - const filterSettings = JSON.parse(filterString); - - setCurrentFilter(filterSettings.currentFilter); - setCurrentSort(filterSettings.currentSort); - setCurrentPageSize(filterSettings.currentPageSize); - } - - // If filter value is provided in query, use that instead - if (Object.values(Filter).includes(router.query.filter as Filter)) { - setCurrentFilter(router.query.filter as Filter); - } - }, [router.query.filter]); - - // Set filter values to local storage any time they are changed - useEffect(() => { - window.localStorage.setItem( - 'rl-filter-settings', - JSON.stringify({ - currentFilter, - currentSort, - currentPageSize, - }) - ); - }, [currentFilter, currentSort, currentPageSize]); - if (!data && !error) { return ; } @@ -102,81 +59,73 @@ const RequestList: React.FC = () => { return ( <> - -
-
- {user?.displayName} - - ) : ( - '' - ) - } - > - {intl.formatMessage(messages.requests)} -
+ +
+
{intl.formatMessage(messages.requests)}
- + + +
- + + + { - setCurrentPageSize(Number(e.target.value)); - router - .push({ - pathname: router.pathname, - query: router.query.userId - ? { userId: router.query.userId } - : {}, - }) - .then(() => window.scrollTo(0, 0)); - }} - value={currentPageSize} - className="inline short" + + + -
+ {intl.formatMessage(messages.previous)} + + +
+ + + + + ); }; diff --git a/src/components/StatusChacker/index.tsx b/src/components/StatusChacker/index.tsx index 82e449db6..10bf0ee00 100644 --- a/src/components/StatusChacker/index.tsx +++ b/src/components/StatusChacker/index.tsx @@ -27,28 +27,7 @@ const StatusChecker: React.FC = () => { return null; } - return ( - - } - title={intl.formatMessage(messages.newversionavailable)} - onOk={() => location.reload()} - okText={intl.formatMessage(messages.reloadOverseerr)} - backgroundClickable={false} - > - {intl.formatMessage(messages.newversionDescription)} - - - ); + return null; }; export default StatusChecker;