import { ChatIcon, CheckCircleIcon, ExclamationIcon, PlayIcon, ServerIcon, } from '@heroicons/react/outline'; import { RefreshIcon } from '@heroicons/react/solid'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import Link from 'next/link'; import { useRouter } from 'next/router'; import React, { 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'; 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 { Permission, useUser } from '../../hooks/useUser'; import globalMessages from '../../i18n/globalMessages'; import Error from '../../pages/_error'; import Badge from '../Common/Badge'; import Button from '../Common/Button'; import CachedImage from '../Common/CachedImage'; import LoadingSpinner from '../Common/LoadingSpinner'; import Modal from '../Common/Modal'; import PageTitle from '../Common/PageTitle'; import { issueOptions } from '../IssueModal/constants'; import Transition from '../Transition'; import IssueComment from './IssueComment'; import IssueDescription from './IssueDescription'; 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: React.FC = () => { 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 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" iconSvg={} > {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;