feat: tv request modal status hookup

pull/132/head
sct 4 years ago
parent 85ae4998f0
commit 5f8114f730

@ -45,6 +45,7 @@ class Media {
try { try {
const media = await mediaRepository.findOne({ const media = await mediaRepository.findOne({
where: { tmdbId: id }, where: { tmdbId: id },
relations: ['requests'],
}); });
return media; return media;
@ -75,7 +76,7 @@ class Media {
@Column({ type: 'int', default: MediaStatus.UNKNOWN }) @Column({ type: 'int', default: MediaStatus.UNKNOWN })
public status: MediaStatus; public status: MediaStatus;
@OneToMany(() => MediaRequest, (request) => request.media) @OneToMany(() => MediaRequest, (request) => request.media, { cascade: true })
public requests: MediaRequest[]; public requests: MediaRequest[];
@CreateDateColumn() @CreateDateColumn()

@ -27,7 +27,9 @@ export class MediaRequest {
@Column({ type: 'integer' }) @Column({ type: 'integer' })
public status: MediaRequestStatus; public status: MediaRequestStatus;
@ManyToOne(() => Media, (media) => media.requests, { eager: true }) @ManyToOne(() => Media, (media) => media.requests, {
eager: true,
})
public media: Media; public media: Media;
@ManyToOne(() => User, (user) => user.requests, { eager: true }) @ManyToOne(() => User, (user) => user.requests, { eager: true })

@ -108,7 +108,9 @@ requestRoutes.post(
const request = new MediaRequest({ const request = new MediaRequest({
type: MediaType.TV, type: MediaType.TV,
media, media: {
id: media.id,
} as Media,
requestedBy: req.user, requestedBy: req.user,
// If the user is an admin or has the "auto approve" permission, automatically approve the request // If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(Permission.AUTO_APPROVE) status: req.user?.hasPermission(Permission.AUTO_APPROVE)

@ -0,0 +1,29 @@
import React from 'react';
interface BadgeProps {
badgeType?: 'default' | 'primary' | 'danger' | 'warning' | 'success';
}
const Badge: React.FC<BadgeProps> = ({ badgeType = 'default', children }) => {
const badgeStyle = [
'px-2 inline-flex text-xs leading-5 font-semibold rounded-full',
];
switch (badgeType) {
case 'danger':
badgeStyle.push('bg-red-600 text-red-100');
break;
case 'warning':
badgeStyle.push('bg-orange-400 text-orange-100');
break;
case 'success':
badgeStyle.push('bg-green-500 text-green-100');
break;
default:
badgeStyle.push('bg-indigo-500 text-indigo-100');
}
return <span className={badgeStyle.join(' ')}>{children}</span>;
};
export default Badge;

@ -20,6 +20,7 @@ import LoadingSpinner from '../Common/LoadingSpinner';
import { useUser, Permission } from '../../hooks/useUser'; import { useUser, Permission } from '../../hooks/useUser';
import { MediaStatus } from '../../../server/constants/media'; import { MediaStatus } from '../../../server/constants/media';
import RequestModal from '../RequestModal'; import RequestModal from '../RequestModal';
import Badge from '../Common/Badge';
const messages = defineMessages({ const messages = defineMessages({
releasedate: 'Release Date', releasedate: 'Release Date',
@ -98,7 +99,6 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
tmdbId={data.id} tmdbId={data.id}
show={showRequestModal} show={showRequestModal}
type="movie" type="movie"
requestId={data.mediaInfo?.requests?.[0]?.id}
onComplete={() => { onComplete={() => {
revalidate(); revalidate();
setShowRequestModal(false); setShowRequestModal(false);
@ -114,10 +114,21 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
/> />
</div> </div>
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left"> <div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
<span className="md:text-2xl md:leading-none"> <div className="mb-2 md:mb-0">
{data.releaseDate.slice(0, 4)} {data.mediaInfo?.status === MediaStatus.AVAILABLE && (
</span> <Badge badgeType="success">Available</Badge>
<h1 className="text-2xl md:text-4xl">{data.title}</h1> )}
{data.mediaInfo?.status === MediaStatus.PROCESSING && (
<Badge badgeType="danger">Unavailable</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
)}
</div>
<h1 className="text-2xl md:text-4xl">
{data.title}{' '}
<span className="text-2xl">({data.releaseDate.slice(0, 4)})</span>
</h1>
<span className="text-xs md:text-base mt-1 md:mt-0"> <span className="text-xs md:text-base mt-1 md:mt-0">
{(data.runtime ?? 0) > 0 && ( {(data.runtime ?? 0) > 0 && (
<> <>

@ -7,8 +7,13 @@ import { MediaRequest } from '../../../server/entity/MediaRequest';
import useSWR from 'swr'; import useSWR from 'swr';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import axios from 'axios'; import axios from 'axios';
import type { MediaStatus } from '../../../server/constants/media'; import {
MediaStatus,
MediaRequestStatus,
} from '../../../server/constants/media';
import { TvDetails, SeasonWithEpisodes } from '../../../server/models/Tv'; import { TvDetails, SeasonWithEpisodes } from '../../../server/models/Tv';
import type SeasonRequest from '../../../server/entity/SeasonRequest';
import Badge from '../Common/Badge';
const messages = defineMessages({ const messages = defineMessages({
requestadmin: 'Your request will be immediately approved.', requestadmin: 'Your request will be immediately approved.',
@ -71,11 +76,24 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
} }
}; };
const getAllRequestedSeasons = (): number[] =>
(data?.mediaInfo?.requests ?? []).reduce((requestedSeasons, request) => {
return [
...requestedSeasons,
...request.seasons.map((sr) => sr.seasonNumber),
];
}, [] as number[]);
const isSelectedSeason = (seasonNumber: number): boolean => { const isSelectedSeason = (seasonNumber: number): boolean => {
return selectedSeasons.includes(seasonNumber); return selectedSeasons.includes(seasonNumber);
}; };
const toggleSeason = (seasonNumber: number): void => { const toggleSeason = (seasonNumber: number): void => {
// If this season already has a pending request, don't allow it to be toggled
if (getAllRequestedSeasons().includes(seasonNumber)) {
return;
}
if (selectedSeasons.includes(seasonNumber)) { if (selectedSeasons.includes(seasonNumber)) {
setSelectedSeasons((seasons) => setSelectedSeasons((seasons) =>
seasons.filter((sn) => sn !== seasonNumber) seasons.filter((sn) => sn !== seasonNumber)
@ -90,11 +108,18 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
data && data &&
selectedSeasons.length >= 0 && selectedSeasons.length >= 0 &&
selectedSeasons.length < selectedSeasons.length <
data?.seasons.filter((season) => season.seasonNumber !== 0).length data?.seasons
.filter((season) => season.seasonNumber !== 0)
.filter(
(season) => !getAllRequestedSeasons().includes(season.seasonNumber)
).length
) { ) {
setSelectedSeasons( setSelectedSeasons(
data.seasons data.seasons
.filter((season) => season.seasonNumber !== 0) .filter((season) => season.seasonNumber !== 0)
.filter(
(season) => !getAllRequestedSeasons().includes(season.seasonNumber)
)
.map((season) => season.seasonNumber) .map((season) => season.seasonNumber)
); );
} else { } else {
@ -108,7 +133,11 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
} }
return ( return (
selectedSeasons.length === selectedSeasons.length ===
data.seasons.filter((season) => season.seasonNumber !== 0).length data.seasons
.filter((season) => season.seasonNumber !== 0)
.filter(
(season) => !getAllRequestedSeasons().includes(season.seasonNumber)
).length
); );
}; };
@ -116,6 +145,23 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
? intl.formatMessage(messages.requestadmin) ? intl.formatMessage(messages.requestadmin)
: undefined; : undefined;
const getSeasonRequest = (
seasonNumber: number
): SeasonRequest | undefined => {
let seasonRequest: SeasonRequest | undefined;
if (data?.mediaInfo && (data.mediaInfo.requests || []).length > 0) {
data.mediaInfo.requests.forEach((request) => {
if (!seasonRequest) {
seasonRequest = request.seasons.find(
(season) => season.seasonNumber === seasonNumber
);
}
});
}
return seasonRequest;
};
return ( return (
<Modal <Modal
visible={visible} visible={visible}
@ -196,54 +242,76 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
<tbody className="bg-cool-gray-600 divide-y"> <tbody className="bg-cool-gray-600 divide-y">
{data?.seasons {data?.seasons
.filter((season) => season.seasonNumber !== 0) .filter((season) => season.seasonNumber !== 0)
.map((season) => ( .map((season) => {
<tr key={`season-${season.id}`}> const seasonRequest = getSeasonRequest(
<td className="px-4 py-4 whitespace-no-wrap text-sm leading-5 font-medium text-gray-100"> season.seasonNumber
<span );
role="checkbox" return (
tabIndex={0} <tr key={`season-${season.id}`}>
aria-checked={isSelectedSeason(season.seasonNumber)} <td className="px-4 py-4 whitespace-no-wrap text-sm leading-5 font-medium text-gray-100">
onClick={() => toggleSeason(season.seasonNumber)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === 'Space') {
toggleSeason(season.seasonNumber);
}
}}
className="group relative inline-flex items-center justify-center flex-shrink-0 h-5 w-10 cursor-pointer focus:outline-none"
>
<span <span
aria-hidden="true" role="checkbox"
className={`${ tabIndex={0}
aria-checked={
!!seasonRequest ||
isSelectedSeason(season.seasonNumber) isSelectedSeason(season.seasonNumber)
? 'bg-indigo-500' }
: 'bg-gray-800' onClick={() => toggleSeason(season.seasonNumber)}
} absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`} onKeyDown={(e) => {
></span> if (e.key === 'Enter' || e.key === 'Space') {
<span toggleSeason(season.seasonNumber);
aria-hidden="true" }
className={`${ }}
isSelectedSeason(season.seasonNumber) className={`group relative inline-flex items-center justify-center flex-shrink-0 h-5 w-10 cursor-pointer focus:outline-none ${
? 'translate-x-5' seasonRequest ? 'opacity-50' : ''
: 'translate-x-0' }`}
} absolute left-0 inline-block h-5 w-5 border border-gray-200 rounded-full bg-white shadow transform group-focus:shadow-outline group-focus:border-blue-300 transition-transform ease-in-out duration-200`} >
></span> <span
</span> aria-hidden="true"
</td> className={`${
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 font-medium text-gray-100"> !!seasonRequest ||
{season.seasonNumber === 0 isSelectedSeason(season.seasonNumber)
? 'Extras' ? 'bg-indigo-500'
: `Season ${season.seasonNumber}`} : 'bg-gray-800'
</td> } absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`}
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-200"> ></span>
{season.episodeCount} <span
</td> aria-hidden="true"
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-200"> className={`${
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-400 text-green-800"> !!seasonRequest ||
Available isSelectedSeason(season.seasonNumber)
</span> ? 'translate-x-5'
</td> : 'translate-x-0'
</tr> } absolute left-0 inline-block h-5 w-5 border border-gray-200 rounded-full bg-white shadow transform group-focus:shadow-outline group-focus:border-blue-300 transition-transform ease-in-out duration-200`}
))} ></span>
</span>
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 font-medium text-gray-100">
{season.seasonNumber === 0
? 'Extras'
: `Season ${season.seasonNumber}`}
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-200">
{season.episodeCount}
</td>
<td className="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-200">
{!seasonRequest && <Badge>Not Requested</Badge>}
{seasonRequest?.status ===
MediaRequestStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
)}
{seasonRequest?.status ===
MediaRequestStatus.APPROVED && (
<Badge badgeType="danger">Unavailable</Badge>
)}
{seasonRequest?.status ===
MediaRequestStatus.AVAILABLE && (
<Badge badgeType="success">Available</Badge>
)}
</td>
</tr>
);
})}
</tbody> </tbody>
</table> </table>
</div> </div>

@ -6,7 +6,6 @@ import type { MediaStatus } from '../../../server/constants/media';
import TvRequestModal from './TvRequestModal'; import TvRequestModal from './TvRequestModal';
interface RequestModalProps { interface RequestModalProps {
requestId?: number;
show: boolean; show: boolean;
type: 'movie' | 'tv'; type: 'movie' | 'tv';
tmdbId: number; tmdbId: number;
@ -18,7 +17,6 @@ interface RequestModalProps {
const RequestModal: React.FC<RequestModalProps> = ({ const RequestModal: React.FC<RequestModalProps> = ({
type, type,
requestId,
show, show,
tmdbId, tmdbId,
onComplete, onComplete,
@ -26,16 +24,12 @@ const RequestModal: React.FC<RequestModalProps> = ({
onUpdating, onUpdating,
onCancel, onCancel,
}) => { }) => {
const { data } = useSWR<MediaRequest>(
requestId ? `/api/v1/request/${requestId}` : null
);
if (type === 'tv') { if (type === 'tv') {
return ( return (
<TvRequestModal <TvRequestModal
onComplete={onComplete} onComplete={onComplete}
onCancel={onCancel} onCancel={onCancel}
visible={show} visible={show}
request={data}
tmdbId={tmdbId} tmdbId={tmdbId}
onUpdating={onUpdating} onUpdating={onUpdating}
/> />
@ -47,7 +41,6 @@ const RequestModal: React.FC<RequestModalProps> = ({
onComplete={onComplete} onComplete={onComplete}
onCancel={onCancel} onCancel={onCancel}
visible={show} visible={show}
request={data}
tmdbId={tmdbId} tmdbId={tmdbId}
onUpdating={onUpdating} onUpdating={onUpdating}
/> />

@ -14,6 +14,7 @@ import { useUser, Permission } from '../../hooks/useUser';
import { TvDetails as TvDetailsType } from '../../../server/models/Tv'; import { TvDetails as TvDetailsType } from '../../../server/models/Tv';
import { MediaStatus } from '../../../server/constants/media'; import { MediaStatus } from '../../../server/constants/media';
import RequestModal from '../RequestModal'; import RequestModal from '../RequestModal';
import Badge from '../Common/Badge';
const messages = defineMessages({ const messages = defineMessages({
userrating: 'User Rating', userrating: 'User Rating',
@ -88,7 +89,6 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
tmdbId={data.id} tmdbId={data.id}
show={showRequestModal} show={showRequestModal}
type="tv" type="tv"
requestId={data.mediaInfo?.requests?.[0]?.id}
onComplete={() => { onComplete={() => {
revalidate(); revalidate();
setShowRequestModal(false); setShowRequestModal(false);
@ -104,17 +104,31 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
/> />
</div> </div>
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left"> <div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
<span className="md:text-2xl md:leading-none"> <div className="mb-2 md:mb-0">
{data.firstAirDate.slice(0, 4)} {data.mediaInfo?.status === MediaStatus.AVAILABLE && (
</span> <Badge badgeType="success">Available</Badge>
<h1 className="text-2xl md:text-4xl">{data.name}</h1> )}
{data.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE && (
<Badge badgeType="success">Partially Available</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PROCESSING && (
<Badge badgeType="danger">Unavailable</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
)}
</div>
<h1 className="text-2xl md:text-4xl">
{data.name}{' '}
<span className="text-2xl">({data.firstAirDate.slice(0, 4)})</span>
</h1>
<span className="text-xs md:text-base mt-1 md:mt-0"> <span className="text-xs md:text-base mt-1 md:mt-0">
{data.genres.map((g) => g.name).join(', ')} {data.genres.map((g) => g.name).join(', ')}
</span> </span>
</div> </div>
<div className="flex-1 flex justify-end mt-4 md:mt-0"> <div className="flex-1 flex justify-end mt-4 md:mt-0">
{(!data.mediaInfo || {(!data.mediaInfo ||
data.mediaInfo.status === MediaStatus.UNKNOWN) && ( data.mediaInfo.status !== MediaStatus.AVAILABLE) && (
<Button <Button
buttonType="primary" buttonType="primary"
onClick={() => setShowRequestModal(true)} onClick={() => setShowRequestModal(true)}
@ -136,63 +150,6 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<FormattedMessage {...messages.request} /> <FormattedMessage {...messages.request} />
</Button> </Button>
)} )}
{data.mediaInfo?.status === MediaStatus.PENDING && (
<Button buttonType="warning">
<svg
className="w-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
<FormattedMessage {...messages.pending} />
</Button>
)}
{data.mediaInfo?.status === MediaStatus.PROCESSING && (
<Button buttonType="danger">
<svg
className="w-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<FormattedMessage {...messages.unavailable} />
</Button>
)}
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Button buttonType="success">
<svg
className="w-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<FormattedMessage {...messages.available} />
</Button>
)}
<Button buttonType="danger" className="ml-2"> <Button buttonType="danger" className="ml-2">
<svg <svg
className="w-5" className="w-5"

Loading…
Cancel
Save