import Badge from '@app/components/Common/Badge'; import Button from '@app/components/Common/Button'; import CachedImage from '@app/components/Common/CachedImage'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import Modal from '@app/components/Common/Modal'; import PageTitle from '@app/components/Common/PageTitle'; import IssueComment from '@app/components/IssueDetails/IssueComment'; import IssueDescription from '@app/components/IssueDetails/IssueDescription'; import { issueOptions } from '@app/components/IssueModal/constants'; import useDeepLinks from '@app/hooks/useDeepLinks'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import Error from '@app/pages/_error'; import { Transition } from '@headlessui/react'; import { ChatIcon, CheckCircleIcon, PlayIcon, ServerIcon, } from '@heroicons/react/outline'; import { RefreshIcon } from '@heroicons/react/solid'; import { IssueStatus } from '@server/constants/issue'; import { MediaType } from '@server/constants/media'; import type Issue from '@server/entity/Issue'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useState } from 'react'; import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; import * as Yup from 'yup'; const messages = defineMessages({ openedby: '#{issueId} opened {relativeTime} by {username}', closeissue: 'Close Issue', closeissueandcomment: 'Close with Comment', leavecomment: 'Comment', comments: 'Comments', reopenissue: 'Reopen Issue', reopenissueandcomment: 'Reopen with Comment', issuepagetitle: 'Issue', playonplex: 'Play on Plex', play4konplex: 'Play in 4K on Plex', openinarr: 'Open in {arr}', openin4karr: 'Open in 4K {arr}', toasteditdescriptionsuccess: 'Issue description edited successfully!', toasteditdescriptionfailed: 'Something went wrong while editing the issue description.', toaststatusupdated: 'Issue status updated successfully!', toaststatusupdatefailed: 'Something went wrong while updating the issue status.', issuetype: 'Type', lastupdated: 'Last Updated', problemseason: 'Affected Season', allseasons: 'All Seasons', season: 'Season {seasonNumber}', problemepisode: 'Affected Episode', allepisodes: 'All Episodes', episode: 'Episode {episodeNumber}', deleteissue: 'Delete Issue', deleteissueconfirm: 'Are you sure you want to delete this issue?', toastissuedeleted: 'Issue deleted successfully!', toastissuedeletefailed: 'Something went wrong while deleting the issue.', nocomments: 'No comments.', unknownissuetype: 'Unknown', commentplaceholder: 'Add a comment…', }); const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { return (movie as MovieDetails).title !== undefined; }; const IssueDetails = () => { const { addToast } = useToasts(); const router = useRouter(); const intl = useIntl(); const [showDeleteModal, setShowDeleteModal] = useState(false); const { user: currentUser, hasPermission } = useUser(); const { data: issueData, mutate: revalidateIssue } = useSWR( `/api/v1/issue/${router.query.issueId}` ); const { data, error } = useSWR( issueData?.media.tmdbId ? `/api/v1/${issueData.media.mediaType}/${issueData.media.tmdbId}` : null ); const { plexUrl, plexUrl4k } = useDeepLinks({ plexUrl: data?.mediaInfo?.plexUrl, plexUrl4k: data?.mediaInfo?.plexUrl4k, iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl, iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k, }); const CommentSchema = Yup.object().shape({ message: Yup.string().required(), }); const issueOption = issueOptions.find( (opt) => opt.issueType === issueData?.issueType ); if (!data && !error) { return ; } if (!data || !issueData) { return ; } const belongsToUser = issueData.createdBy.id === currentUser?.id; const [firstComment, ...otherComments] = issueData.comments; const editFirstComment = async (newMessage: string) => { try { await axios.put(`/api/v1/issueComment/${firstComment.id}`, { message: newMessage, }); addToast(intl.formatMessage(messages.toasteditdescriptionsuccess), { appearance: 'success', autoDismiss: true, }); revalidateIssue(); } catch (e) { addToast(intl.formatMessage(messages.toasteditdescriptionfailed), { appearance: 'error', autoDismiss: true, }); } }; const updateIssueStatus = async (newStatus: 'open' | 'resolved') => { try { await axios.post(`/api/v1/issue/${issueData.id}/${newStatus}`); addToast(intl.formatMessage(messages.toaststatusupdated), { appearance: 'success', autoDismiss: true, }); revalidateIssue(); } catch (e) { addToast(intl.formatMessage(messages.toaststatusupdatefailed), { appearance: 'error', autoDismiss: true, }); } }; const deleteIssue = async () => { try { await axios.delete(`/api/v1/issue/${issueData.id}`); addToast(intl.formatMessage(messages.toastissuedeleted), { appearance: 'success', autoDismiss: true, }); router.push('/issues'); } catch (e) { addToast(intl.formatMessage(messages.toastissuedeletefailed), { appearance: 'error', autoDismiss: true, }); } }; const title = isMovie(data) ? data.title : data.name; const releaseYear = isMovie(data) ? data.releaseDate : data.firstAirDate; return (
setShowDeleteModal(false)} onOk={() => deleteIssue()} okText={intl.formatMessage(messages.deleteissue)} okButtonType="danger" > {intl.formatMessage(messages.deleteissueconfirm)} {data.backdropPath && (
)}
{issueData.status === IssueStatus.OPEN && ( {intl.formatMessage(globalMessages.open)} )} {issueData.status === IssueStatus.RESOLVED && ( {intl.formatMessage(globalMessages.resolved)} )}

{title} {' '} {releaseYear && ( ({releaseYear.slice(0, 4)}) )}

{intl.formatMessage(messages.openedby, { issueId: issueData.id, username: ( {issueData.createdBy.displayName} ), relativeTime: ( ), })}
{ editFirstComment(newMessage); }} onDelete={() => setShowDeleteModal(true)} />
{intl.formatMessage(messages.issuetype)} {intl.formatMessage( issueOption?.name ?? messages.unknownissuetype )}
{issueData.media.mediaType === MediaType.TV && ( <>
{intl.formatMessage(messages.problemseason)} {intl.formatMessage( issueData.problemSeason > 0 ? messages.season : messages.allseasons, { seasonNumber: issueData.problemSeason } )}
{issueData.problemSeason > 0 && (
{intl.formatMessage(messages.problemepisode)} {intl.formatMessage( issueData.problemEpisode > 0 ? messages.episode : messages.allepisodes, { episodeNumber: issueData.problemEpisode } )}
)} )}
{intl.formatMessage(messages.lastupdated)}
{issueData?.media.plexUrl && ( )} {issueData?.media.serviceUrl && hasPermission(Permission.ADMIN) && ( )} {issueData?.media.plexUrl4k && ( )} {issueData?.media.serviceUrl4k && hasPermission(Permission.ADMIN) && ( )}
{intl.formatMessage(messages.comments)}
{otherComments.map((comment) => ( revalidateIssue()} /> ))} {otherComments.length === 0 && (
{intl.formatMessage(messages.nocomments)}
)} {(hasPermission(Permission.MANAGE_ISSUES) || belongsToUser) && ( { await axios.post(`/api/v1/issue/${issueData?.id}/comment`, { message: values.message, }); revalidateIssue(); resetForm(); }} > {({ isValid, isSubmitting, values, handleSubmit }) => { return (
{hasPermission(Permission.MANAGE_ISSUES) && ( <> {issueData.status === IssueStatus.OPEN ? ( ) : ( )} )}
); }}
)}
{intl.formatMessage(messages.issuetype)} {intl.formatMessage( issueOption?.name ?? messages.unknownissuetype )}
{issueData.media.mediaType === MediaType.TV && ( <>
{intl.formatMessage(messages.problemseason)} {intl.formatMessage( issueData.problemSeason > 0 ? messages.season : messages.allseasons, { seasonNumber: issueData.problemSeason } )}
{issueData.problemSeason > 0 && (
{intl.formatMessage(messages.problemepisode)} {intl.formatMessage( issueData.problemEpisode > 0 ? messages.episode : messages.allepisodes, { episodeNumber: issueData.problemEpisode } )}
)} )}
{intl.formatMessage(messages.lastupdated)}
{issueData?.media.plexUrl && ( )} {issueData?.media.serviceUrl && hasPermission(Permission.ADMIN) && ( )} {issueData?.media.plexUrl4k && ( )} {issueData?.media.serviceUrl4k && hasPermission(Permission.ADMIN) && ( )}
); }; export default IssueDetails;