From d7b9b1a525ec6d1d81ad6fe4e55994dd8428988f Mon Sep 17 00:00:00 2001 From: TheCatLady <52870424+TheCatLady@users.noreply.github.com> Date: Thu, 13 May 2021 07:51:20 -0400 Subject: [PATCH] feat(ui): request list item & request card improvements (#1532) * feat(ui): add additional request card buttons * feat(ui): add year to request list items & request cards * fix(ui): do not show edit button conditionally, and don't hide modifiedBy user * revert: do not unhide season list for reg users on mobile --- src/components/RequestCard/index.tsx | 410 ++++++++++++------ .../RequestList/RequestItem/index.tsx | 141 +++--- src/i18n/locale/en.json | 2 + src/styles/globals.css | 2 +- 4 files changed, 359 insertions(+), 196 deletions(-) diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index afa6216e6..2c23a43de 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -1,9 +1,16 @@ -import { CheckIcon, TrashIcon, XIcon } from '@heroicons/react/solid'; +import { + CheckIcon, + PencilIcon, + RefreshIcon, + TrashIcon, + XIcon, +} from '@heroicons/react/solid'; import axios from 'axios'; import Link from 'next/link'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } 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, @@ -18,10 +25,12 @@ import { withProperties } from '../../utils/typeHelpers'; import Badge from '../Common/Badge'; import Button from '../Common/Button'; import CachedImage from '../Common/CachedImage'; +import RequestModal from '../RequestModal'; 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', }); @@ -89,7 +98,10 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { triggerOnce: true, }); const intl = useIntl(); - const { hasPermission } = useUser(); + const { user, hasPermission } = useUser(); + const { addToast } = useToasts(); + const [isRetrying, setRetrying] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); const url = request.type === 'movie' ? `/api/v1/movie/${request.media.tmdbId}` @@ -113,6 +125,30 @@ 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); @@ -136,25 +172,224 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { } return ( -
- {title.backdropPath && ( -
- -
+ <> + setShowEditModal(false)} + onComplete={() => { + revalidate(); + setShowEditModal(false); + }} + /> +
+ {title.backdropPath && ( +
+ +
+
+ )} +
+
+ {(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, + })} + + {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.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 && ( + + )} +
- )} -
= ({ request, onTitleData }) => { : `/tv/${requestData.media.tmdbId}` } > - - {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.status === MediaRequestStatus.PENDING && - hasPermission(Permission.MANAGE_REQUESTS) && ( -
- - -
- )}
- - - - - -
+ ); }; diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 6642f54d6..8127eeb24 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -32,6 +32,7 @@ const messages = defineMessages({ seasons: '{seasonCount, plural, one {Season} other {Seasons}}', 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.', @@ -219,33 +220,23 @@ const RequestItem: React.FC = ({
- -
- - - - - {requestData.requestedBy.displayName} - - - +
+ {(isMovie(title) + ? title.releaseDate + : title.firstAirDate + )?.slice(0, 4)}
+ + + {isMovie(title) ? title.title : title.name} + + {!isMovie(title) && request.seasons.length > 0 && (
@@ -276,7 +267,7 @@ const RequestItem: React.FC = ({ )}
-
+
{intl.formatMessage(globalMessages.status)} @@ -308,29 +299,20 @@ const RequestItem: React.FC = ({ )}
- - {intl.formatMessage(messages.requested)} - - - {intl.formatDate(requestData.createdAt, { - year: 'numeric', - month: 'long', - day: 'numeric', - })} - -
-
- - {intl.formatMessage(messages.modified)} - - - {requestData.modifiedBy ? ( - + {hasPermission( + [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], + { type: 'or' } + ) ? ( + <> + + {intl.formatMessage(messages.requested)} + + {intl.formatMessage(messages.modifieduserdate, { date: ( = ({ /> ), user: ( - - + + - {requestData.modifiedBy.displayName} + {requestData.requestedBy.displayName} ), })} - ) : ( - N/A - )} - + + ) : ( + <> + + {intl.formatMessage(messages.requesteddate)} + + + + + + )}
+ {requestData.modifiedBy && ( +
+ + {intl.formatMessage(messages.modified)} + + + {intl.formatMessage(messages.modifieduserdate, { + date: ( + + ), + user: ( + + + + + {requestData.modifiedBy.displayName} + + + + ), + })} + +
+ )}
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 8e12649c4..f5a53089b 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -168,6 +168,7 @@ "components.RequestButton.viewrequest": "View Request", "components.RequestButton.viewrequest4k": "View 4K Request", "components.RequestCard.deleterequest": "Delete Request", + "components.RequestCard.failedretry": "Something went wrong while retrying the request.", "components.RequestCard.mediaerror": "The associated title for this request is no longer available.", "components.RequestCard.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestList.RequestItem.cancelRequest": "Cancel Request", @@ -178,6 +179,7 @@ "components.RequestList.RequestItem.modified": "Modified", "components.RequestList.RequestItem.modifieduserdate": "{date} by {user}", "components.RequestList.RequestItem.requested": "Requested", + "components.RequestList.RequestItem.requesteddate": "Requested", "components.RequestList.RequestItem.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestList.requests": "Requests", "components.RequestList.showallrequests": "Show All Requests", diff --git a/src/styles/globals.css b/src/styles/globals.css index 73e5ef20c..c18260b92 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -198,7 +198,7 @@ img.avatar-sm { } .card-field { - @apply flex items-center py-0.5 sm:py-1 text-sm; + @apply flex items-center py-0.5 sm:py-1 text-sm truncate; } .card-field-name {