feat: manage series slideover added (and approve/decline/delete hooked up)

pull/179/head
sct 4 years ago
parent cb9ae25d94
commit 236c4e5e61

@ -37,7 +37,7 @@ export class MediaRequest {
@ManyToOne(() => User, (user) => user.requests, { eager: true }) @ManyToOne(() => User, (user) => user.requests, { eager: true })
public requestedBy: User; public requestedBy: User;
@ManyToOne(() => User, { nullable: true }) @ManyToOne(() => User, { nullable: true, cascade: true, eager: true })
public modifiedBy?: User; public modifiedBy?: User;
@CreateDateColumn() @CreateDateColumn()
@ -118,8 +118,11 @@ export class MediaRequest {
@AfterRemove() @AfterRemove()
private async handleRemoveParentUpdate() { private async handleRemoveParentUpdate() {
const mediaRepository = getRepository(Media); const mediaRepository = getRepository(Media);
if (!this.media.requests || this.media.requests.length === 0) { const fullMedia = await mediaRepository.findOneOrFail({
this.media.status = MediaStatus.UNKNOWN; where: { id: this.media.id },
});
if (!fullMedia.requests || fullMedia.requests.length === 0) {
fullMedia.status = MediaStatus.UNKNOWN;
mediaRepository.save(this.media); mediaRepository.save(this.media);
} }
} }

@ -20,7 +20,9 @@ class SeasonRequest {
@Column({ type: 'int', default: MediaRequestStatus.PENDING }) @Column({ type: 'int', default: MediaRequestStatus.PENDING })
public status: MediaRequestStatus; public status: MediaRequestStatus;
@ManyToOne(() => MediaRequest, (request) => request.seasons) @ManyToOne(() => MediaRequest, (request) => request.seasons, {
onDelete: 'CASCADE',
})
public request: MediaRequest; public request: MediaRequest;
@CreateDateColumn() @CreateDateColumn()

@ -54,14 +54,14 @@ requestRoutes.get('/', async (req, res, next) => {
const requests = req.user?.hasPermission(Permission.MANAGE_REQUESTS) const requests = req.user?.hasPermission(Permission.MANAGE_REQUESTS)
? await requestRepository.find({ ? await requestRepository.find({
order: sortFilter, order: sortFilter,
relations: ['media'], relations: ['media', 'modifiedBy'],
where: { status: statusFilter }, where: { status: statusFilter },
take: Number(req.query.take) ?? 20, take: Number(req.query.take) ?? 20,
skip: Number(req.query.skip) ?? 0, skip: Number(req.query.skip) ?? 0,
}) })
: await requestRepository.find({ : await requestRepository.find({
where: { requestedBy: { id: req.user?.id }, status: statusFilter }, where: { requestedBy: { id: req.user?.id }, status: statusFilter },
relations: ['media'], relations: ['media', 'modifiedBy'],
order: sortFilter, order: sortFilter,
take: Number(req.query.limit) ?? 20, take: Number(req.query.limit) ?? 20,
skip: Number(req.query.skip) ?? 0, skip: Number(req.query.skip) ?? 0,
@ -116,6 +116,9 @@ requestRoutes.post(
status: req.user?.hasPermission(Permission.AUTO_APPROVE) status: req.user?.hasPermission(Permission.AUTO_APPROVE)
? MediaRequestStatus.APPROVED ? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING, : MediaRequestStatus.PENDING,
modifiedBy: req.user?.hasPermission(Permission.AUTO_APPROVE)
? req.user
: undefined,
}); });
await requestRepository.save(request); await requestRepository.save(request);
@ -158,6 +161,9 @@ requestRoutes.post(
status: req.user?.hasPermission(Permission.AUTO_APPROVE) status: req.user?.hasPermission(Permission.AUTO_APPROVE)
? MediaRequestStatus.APPROVED ? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING, : MediaRequestStatus.PENDING,
modifiedBy: req.user?.hasPermission(Permission.AUTO_APPROVE)
? req.user
: undefined,
seasons: finalSeasons.map( seasons: finalSeasons.map(
(sn) => (sn) =>
new SeasonRequest({ new SeasonRequest({
@ -254,6 +260,7 @@ requestRoutes.get<{
} }
request.status = newStatus; request.status = newStatus;
request.modifiedBy = req.user;
await requestRepository.save(request); await requestRepository.save(request);
return res.status(200).json(request); return res.status(200).json(request);

@ -27,6 +27,7 @@ import Badge from '../Common/Badge';
import ButtonWithDropdown from '../Common/ButtonWithDropdown'; import ButtonWithDropdown from '../Common/ButtonWithDropdown';
import axios from 'axios'; import axios from 'axios';
import SlideOver from '../Common/SlideOver'; import SlideOver from '../Common/SlideOver';
import RequestBlock from '../RequestBlock';
const messages = defineMessages({ const messages = defineMessages({
releasedate: 'Release Date', releasedate: 'Release Date',
@ -130,81 +131,18 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<div className="bg-cool-gray-600 shadow overflow-hidden rounded-md"> <div className="bg-cool-gray-600 shadow overflow-hidden rounded-md">
<ul> <ul>
{data.mediaInfo?.requests?.map((request) => ( {data.mediaInfo?.requests?.map((request) => (
<li key={`manage-request-${request.id}`}> <li
<div className="block"> key={`manage-request-${request.id}`}
<div className="px-4 py-4 sm:px-6"> className="border-b last:border-b-0 border-cool-gray-700"
<div className="flex items-center justify-between">
<div className="mr-6 flex items-center text-sm leading-5 text-gray-300">
<svg
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
> >
<path <RequestBlock request={request} onUpdate={() => revalidate()} />
fillRule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clipRule="evenodd"
/>
</svg>
{request.requestedBy.username}
</div>
<div className="ml-2 flex-shrink-0 flex">
<Button buttonSize="sm" buttonType="danger">
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
</Button>
</div>
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="sm:flex">
<div className="mr-6 flex items-center text-sm leading-5 text-gray-300">
{request.status === MediaRequestStatus.AVAILABLE && (
<Badge badgeType="success">Available</Badge>
)}
{request.status === MediaRequestStatus.APPROVED && (
<Badge badgeType="success">Approved</Badge>
)}
{request.status === MediaRequestStatus.DECLINED && (
<Badge badgeType="danger">Declined</Badge>
)}
{request.status === MediaRequestStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
)}
</div>
</div>
<div className="mt-2 flex items-center text-sm leading-5 text-gray-300 sm:mt-0">
<svg
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
clipRule="evenodd"
/>
</svg>
<span>
<FormattedDate value={request.createdAt} />
</span>
</div>
</div>
</div>
</div>
</li> </li>
))} ))}
{(data.mediaInfo?.requests ?? []).length === 0 && (
<li className="text-center py-4 text-cool-gray-400">
No requests
</li>
)}
</ul> </ul>
</div> </div>
</SlideOver> </SlideOver>

@ -0,0 +1,192 @@
import React, { useState } from 'react';
import type { MediaRequest } from '../../../server/entity/MediaRequest';
import { FormattedDate } from 'react-intl';
import Badge from '../Common/Badge';
import { MediaRequestStatus } from '../../../server/constants/media';
import Button from '../Common/Button';
import axios from 'axios';
interface RequestBlockProps {
request: MediaRequest;
onUpdate?: () => void;
}
const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
const [isUpdating, setIsUpdating] = useState(false);
const updateRequest = async (type: 'approve' | 'decline'): Promise<void> => {
setIsUpdating(true);
await axios.get(`/api/v1/request/${request.id}/${type}`);
if (onUpdate) {
onUpdate();
}
setIsUpdating(false);
};
const deleteRequest = async () => {
setIsUpdating(true);
await axios.delete(`/api/v1/request/${request.id}`);
if (onUpdate) {
onUpdate();
}
setIsUpdating(false);
};
return (
<div className="block">
<div className="px-4 py-4 sm:px-6 ">
<div className="flex items-center justify-between">
<div className="mr-6 flex items-center text-sm leading-5 text-gray-300">
<svg
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clipRule="evenodd"
/>
</svg>
<span>{request.requestedBy.username}</span>
{request.modifiedBy && (
<>
<svg
className="flex-shrink-0 ml-2 mr-1.5 h-5 w-5 text-gray-300"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path
fillRule="evenodd"
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
clipRule="evenodd"
/>
</svg>
<span>{request.modifiedBy?.username}</span>
</>
)}
</div>
<div className="ml-2 flex-shrink-0 flex">
{request.status === MediaRequestStatus.PENDING && (
<>
<span className="mr-1">
<Button
buttonType="success"
onClick={() => updateRequest('approve')}
disabled={isUpdating}
>
<svg
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</Button>
</span>
<span>
<Button
buttonType="danger"
onClick={() => updateRequest('decline')}
disabled={isUpdating}
>
<svg
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Button>
</span>
</>
)}
{request.status !== MediaRequestStatus.PENDING && (
<Button
buttonType="danger"
onClick={() => deleteRequest()}
disabled={isUpdating}
>
<svg
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
</Button>
)}
</div>
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="sm:flex">
<div className="mr-6 flex items-center text-sm leading-5 text-gray-300">
{request.status === MediaRequestStatus.AVAILABLE && (
<Badge badgeType="success">Available</Badge>
)}
{request.status === MediaRequestStatus.APPROVED && (
<Badge badgeType="success">Approved</Badge>
)}
{request.status === MediaRequestStatus.DECLINED && (
<Badge badgeType="danger">Declined</Badge>
)}
{request.status === MediaRequestStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
)}
</div>
</div>
<div className="mt-2 flex items-center text-sm leading-5 text-gray-300 sm:mt-0">
<svg
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
clipRule="evenodd"
/>
</svg>
<span>
<FormattedDate value={request.createdAt} />
</span>
</div>
</div>
{(request.seasons ?? []).length > 0 && (
<div className="mt-2 text-sm flex items-center">
<span className="mr-2">Seasons</span>
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
)}
</div>
</div>
);
};
export default RequestBlock;

@ -17,6 +17,9 @@ import RequestModal from '../RequestModal';
import Badge from '../Common/Badge'; import Badge from '../Common/Badge';
import ButtonWithDropdown from '../Common/ButtonWithDropdown'; import ButtonWithDropdown from '../Common/ButtonWithDropdown';
import axios from 'axios'; import axios from 'axios';
import SlideOver from '../Common/SlideOver';
import RequestBlock from '../RequestBlock';
import Error from '../../pages/_error';
const messages = defineMessages({ const messages = defineMessages({
userrating: 'User Rating', userrating: 'User Rating',
@ -58,11 +61,12 @@ enum MediaRequestStatus {
} }
const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => { const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
const { user, hasPermission } = useUser(); const { hasPermission } = useUser();
const router = useRouter(); const router = useRouter();
const intl = useIntl(); const intl = useIntl();
const { locale } = useContext(LanguageContext); const { locale } = useContext(LanguageContext);
const [showRequestModal, setShowRequestModal] = useState(false); const [showRequestModal, setShowRequestModal] = useState(false);
const [showManager, setShowManager] = useState(false);
const { data, error, revalidate } = useSWR<TvDetailsType>( const { data, error, revalidate } = useSWR<TvDetailsType>(
`/api/v1/tv/${router.query.tvId}?language=${locale}`, `/api/v1/tv/${router.query.tvId}?language=${locale}`,
{ {
@ -81,7 +85,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
} }
if (!data) { if (!data) {
return <div>Broken?</div>; return <Error statusCode={404} />;
} }
const activeRequests = data.mediaInfo?.requests?.filter( const activeRequests = data.mediaInfo?.requests?.filter(
@ -120,6 +124,31 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
}} }}
onCancel={() => setShowRequestModal(false)} onCancel={() => setShowRequestModal(false)}
/> />
<SlideOver
show={showManager}
title="Manage Series"
onClose={() => setShowManager(false)}
subText={data.name}
>
<h3 className="text-xl mb-2">Requests</h3>
<div className="bg-cool-gray-600 shadow overflow-hidden rounded-md">
<ul>
{data.mediaInfo?.requests?.map((request) => (
<li
key={`manage-request-${request.id}`}
className="border-b last:border-b-0 border-cool-gray-700"
>
<RequestBlock request={request} onUpdate={() => revalidate()} />
</li>
))}
{(data.mediaInfo?.requests ?? []).length === 0 && (
<li className="text-center py-4 text-cool-gray-400">
No requests
</li>
)}
</ul>
</div>
</SlideOver>
<div className="flex flex-col items-center md:flex-row md:items-end pt-4"> <div className="flex flex-col items-center md:flex-row md:items-end pt-4">
<div className="mr-4 flex-shrink-0"> <div className="mr-4 flex-shrink-0">
<img <img
@ -259,7 +288,11 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
</ButtonWithDropdown> </ButtonWithDropdown>
)} )}
{hasPermission(Permission.MANAGE_REQUESTS) && ( {hasPermission(Permission.MANAGE_REQUESTS) && (
<Button buttonType="default" className="ml-2"> <Button
buttonType="default"
className="ml-2"
onClick={() => setShowManager(true)}
>
<svg <svg
className="w-5" className="w-5"
style={{ height: 20 }} style={{ height: 20 }}

Loading…
Cancel
Save