diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index 0ba09fb7b..987b05393 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -37,7 +37,7 @@ export class MediaRequest { @ManyToOne(() => User, (user) => user.requests, { eager: true }) public requestedBy: User; - @ManyToOne(() => User, { nullable: true }) + @ManyToOne(() => User, { nullable: true, cascade: true, eager: true }) public modifiedBy?: User; @CreateDateColumn() @@ -118,8 +118,11 @@ export class MediaRequest { @AfterRemove() private async handleRemoveParentUpdate() { const mediaRepository = getRepository(Media); - if (!this.media.requests || this.media.requests.length === 0) { - this.media.status = MediaStatus.UNKNOWN; + const fullMedia = await mediaRepository.findOneOrFail({ + where: { id: this.media.id }, + }); + if (!fullMedia.requests || fullMedia.requests.length === 0) { + fullMedia.status = MediaStatus.UNKNOWN; mediaRepository.save(this.media); } } diff --git a/server/entity/SeasonRequest.ts b/server/entity/SeasonRequest.ts index 846bbe164..f499406c5 100644 --- a/server/entity/SeasonRequest.ts +++ b/server/entity/SeasonRequest.ts @@ -20,7 +20,9 @@ class SeasonRequest { @Column({ type: 'int', default: MediaRequestStatus.PENDING }) public status: MediaRequestStatus; - @ManyToOne(() => MediaRequest, (request) => request.seasons) + @ManyToOne(() => MediaRequest, (request) => request.seasons, { + onDelete: 'CASCADE', + }) public request: MediaRequest; @CreateDateColumn() diff --git a/server/routes/request.ts b/server/routes/request.ts index 04d896e3d..f7bf32d61 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -54,14 +54,14 @@ requestRoutes.get('/', async (req, res, next) => { const requests = req.user?.hasPermission(Permission.MANAGE_REQUESTS) ? await requestRepository.find({ order: sortFilter, - relations: ['media'], + relations: ['media', 'modifiedBy'], where: { status: statusFilter }, take: Number(req.query.take) ?? 20, skip: Number(req.query.skip) ?? 0, }) : await requestRepository.find({ where: { requestedBy: { id: req.user?.id }, status: statusFilter }, - relations: ['media'], + relations: ['media', 'modifiedBy'], order: sortFilter, take: Number(req.query.limit) ?? 20, skip: Number(req.query.skip) ?? 0, @@ -116,6 +116,9 @@ requestRoutes.post( status: req.user?.hasPermission(Permission.AUTO_APPROVE) ? MediaRequestStatus.APPROVED : MediaRequestStatus.PENDING, + modifiedBy: req.user?.hasPermission(Permission.AUTO_APPROVE) + ? req.user + : undefined, }); await requestRepository.save(request); @@ -158,6 +161,9 @@ requestRoutes.post( status: req.user?.hasPermission(Permission.AUTO_APPROVE) ? MediaRequestStatus.APPROVED : MediaRequestStatus.PENDING, + modifiedBy: req.user?.hasPermission(Permission.AUTO_APPROVE) + ? req.user + : undefined, seasons: finalSeasons.map( (sn) => new SeasonRequest({ @@ -254,6 +260,7 @@ requestRoutes.get<{ } request.status = newStatus; + request.modifiedBy = req.user; await requestRepository.save(request); return res.status(200).json(request); diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 41b7d76cb..bec6167a0 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -27,6 +27,7 @@ import Badge from '../Common/Badge'; import ButtonWithDropdown from '../Common/ButtonWithDropdown'; import axios from 'axios'; import SlideOver from '../Common/SlideOver'; +import RequestBlock from '../RequestBlock'; const messages = defineMessages({ releasedate: 'Release Date', @@ -130,81 +131,18 @@ const MovieDetails: React.FC = ({ movie }) => {
diff --git a/src/components/RequestBlock/index.tsx b/src/components/RequestBlock/index.tsx new file mode 100644 index 000000000..fd90c93bf --- /dev/null +++ b/src/components/RequestBlock/index.tsx @@ -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 = ({ request, onUpdate }) => { + const [isUpdating, setIsUpdating] = useState(false); + + const updateRequest = async (type: 'approve' | 'decline'): Promise => { + 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 ( +
+
+
+
+ + + + {request.requestedBy.username} + {request.modifiedBy && ( + <> + + + + + {request.modifiedBy?.username} + + )} +
+
+ {request.status === MediaRequestStatus.PENDING && ( + <> + + + + + + + + )} + {request.status !== MediaRequestStatus.PENDING && ( + + )} +
+
+
+
+
+ {request.status === MediaRequestStatus.AVAILABLE && ( + Available + )} + {request.status === MediaRequestStatus.APPROVED && ( + Approved + )} + {request.status === MediaRequestStatus.DECLINED && ( + Declined + )} + {request.status === MediaRequestStatus.PENDING && ( + Pending + )} +
+
+
+ + + + + + +
+
+ {(request.seasons ?? []).length > 0 && ( +
+ Seasons + {request.seasons.map((season) => ( + + {season.seasonNumber} + + ))} +
+ )} +
+
+ ); +}; + +export default RequestBlock; diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 487765b95..8d21ae5ea 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -17,6 +17,9 @@ import RequestModal from '../RequestModal'; import Badge from '../Common/Badge'; import ButtonWithDropdown from '../Common/ButtonWithDropdown'; import axios from 'axios'; +import SlideOver from '../Common/SlideOver'; +import RequestBlock from '../RequestBlock'; +import Error from '../../pages/_error'; const messages = defineMessages({ userrating: 'User Rating', @@ -58,11 +61,12 @@ enum MediaRequestStatus { } const TvDetails: React.FC = ({ tv }) => { - const { user, hasPermission } = useUser(); + const { hasPermission } = useUser(); const router = useRouter(); const intl = useIntl(); const { locale } = useContext(LanguageContext); const [showRequestModal, setShowRequestModal] = useState(false); + const [showManager, setShowManager] = useState(false); const { data, error, revalidate } = useSWR( `/api/v1/tv/${router.query.tvId}?language=${locale}`, { @@ -81,7 +85,7 @@ const TvDetails: React.FC = ({ tv }) => { } if (!data) { - return
Broken?
; + return ; } const activeRequests = data.mediaInfo?.requests?.filter( @@ -120,6 +124,31 @@ const TvDetails: React.FC = ({ tv }) => { }} onCancel={() => setShowRequestModal(false)} /> + setShowManager(false)} + subText={data.name} + > +

Requests

+
+
    + {data.mediaInfo?.requests?.map((request) => ( +
  • + revalidate()} /> +
  • + ))} + {(data.mediaInfo?.requests ?? []).length === 0 && ( +
  • + No requests +
  • + )} +
+
+
= ({ tv }) => { )} {hasPermission(Permission.MANAGE_REQUESTS) && ( -