Skip to content

Commit

Permalink
Add release management operations
Browse files Browse the repository at this point in the history
  • Loading branch information
harshithmohan committed Apr 21, 2024
1 parent e5e4bef commit 4f851b9
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 41 deletions.
2 changes: 1 addition & 1 deletion src/components/Input/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const Checkbox = memo((props: Props) => {
</TransitionDiv>
)}
{labelRight && (
<span className="ml-2 flex items-center font-semibold">
<span className="ml-2 flex items-center">
{label}
</span>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Input/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function Select(props: Props) {
id={id}
value={value}
onChange={onChange}
className="w-full appearance-none rounded-lg border border-panel-border bg-panel-input px-4 py-2 transition ease-in-out focus:shadow-none focus:outline-none focus:ring-2 focus:ring-inset focus:ring-panel-icon-action"
className="w-full appearance-none rounded-lg border border-panel-border bg-panel-input py-2 pl-4 pr-8 transition ease-in-out focus:shadow-none focus:outline-none focus:ring-2 focus:ring-inset focus:ring-panel-icon-action"
>
{children}
</select>
Expand Down
21 changes: 2 additions & 19 deletions src/components/Input/SelectEpisodeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import cx from 'classnames';
import { find, toInteger } from 'lodash';

import { EpisodeTypeEnum } from '@/core/types/api/episode';
import getEpisodePrefix from '@/core/utilities/getEpisodePrefix';

import Input from './Input';

Expand Down Expand Up @@ -56,24 +57,6 @@ type Props = {
rowIdx: number;
};

const getPrefix = (type: EpisodeTypeEnum) => {
switch (type) {
case EpisodeTypeEnum.Special:
return 'S';
case EpisodeTypeEnum.ThemeSong:
return 'C';
case EpisodeTypeEnum.Trailer:
return 'T';
case EpisodeTypeEnum.Other:
return 'O';
case EpisodeTypeEnum.Parody:
return 'P';
case EpisodeTypeEnum.Normal:
default:
return '';
}
};

const SelectOption = (option: Option & { divider: boolean }) => (
<Listbox.Option
value={option}
Expand All @@ -83,7 +66,7 @@ const SelectOption = (option: Option & { divider: boolean }) => (
<div className="flex items-center justify-between">
<span className="flex grow truncate font-normal">
<div className="w-10 shrink-0 text-panel-text-important">
{getPrefix(option.type) + option.number}
{getEpisodePrefix(option.type) + option.number}
</div>
|
<div className="ml-2">{option.label}</div>
Expand Down
7 changes: 4 additions & 3 deletions src/components/Utilities/ReleaseManagement/EpisodeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Icon } from '@mdi/react';

import UtilitiesTable from '@/components/Utilities/UtilitiesTable';
import { useSeriesEpisodesWithMultipleReleases } from '@/core/react-query/release-management/queries';
import getEpisodePrefix from '@/core/utilities/getEpisodePrefix';
import useFlattenListResult from '@/hooks/useFlattenListResult';

import type { UtilityHeaderType } from '@/components/Utilities/constants';
Expand All @@ -15,7 +16,7 @@ const columns: UtilityHeaderType<EpisodeType>[] = [
id: 'episode',
name: 'Episode Name',
className: 'line-clamp-1 grow basis-0 overflow-hidden',
item: episode => `${episode.AniDB?.EpisodeNumber} - ${episode.Name}`,
item: episode => `${getEpisodePrefix(episode.AniDB?.Type)}${episode.AniDB?.EpisodeNumber} - ${episode.Name}`,
},
{
id: 'file-count',
Expand All @@ -33,11 +34,11 @@ const columns: UtilityHeaderType<EpisodeType>[] = [
},
];

const EpisodeList = ({ seriesId }: { seriesId: number }) => {
const EpisodeList = ({ ignoreVariations, seriesId }: { ignoreVariations: boolean, seriesId: number }) => {
const navigate = useNavigate();
const episodesQuery = useSeriesEpisodesWithMultipleReleases(
seriesId,
{ includeDataFrom: ['AniDB'], includeAbsolutePaths: true, pageSize: 25 },
{ ignoreVariations, includeDataFrom: ['AniDB'], includeAbsolutePaths: true, pageSize: 25 },
seriesId > 0,
);
const [episodes, episodeCount] = useFlattenListResult(episodesQuery.data);
Expand Down
6 changes: 5 additions & 1 deletion src/components/Utilities/Unrecognized/MenuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ const MenuButton = React.memo((
disabled?: boolean;
},
) => (
<Button onClick={onClick} className="flex items-center gap-x-2 text-panel-text" disabled={disabled}>
<Button
onClick={onClick}
className="flex items-center gap-x-2 !text-base !font-normal text-panel-text"
disabled={disabled}
>
<Icon path={icon} size={1} className={cx({ 'text-panel-text-primary': highlight })} />
{name}
</Button>
Expand Down
7 changes: 7 additions & 0 deletions src/core/react-query/file/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
IgnoreFileRequestType,
LinkManyFilesToOneEpisodeRequestType,
LinkOneFileToManyEpisodesRequestType,
MarkVariationRequestType,
} from '@/core/react-query/file/types';

export const useDeleteFileMutation = () =>
Expand Down Expand Up @@ -58,3 +59,9 @@ export const useRescanFileMutation = () =>
useMutation({
mutationFn: (fileId: number) => axios.post(`File/${fileId}/Rescan`),
});

export const useMarkVariationMutation = () =>
useMutation({
mutationFn: ({ fileId, variation }: MarkVariationRequestType) =>
axios.put(`File/${fileId}/Variation`, undefined, { params: { value: variation } }),
});
5 changes: 5 additions & 0 deletions src/core/react-query/file/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ export type LinkManyFilesToOneEpisodeRequestType = {
episodeID: number;
fileIDs: number[];
};

export type MarkVariationRequestType = {
fileId: number;
variation: boolean;
};
21 changes: 21 additions & 0 deletions src/core/utilities/getEpisodePrefix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { EpisodeTypeEnum } from '@/core/types/api/episode';

const getEpisodePrefix = (type?: EpisodeTypeEnum) => {
switch (type) {
case EpisodeTypeEnum.Special:
return 'S';
case EpisodeTypeEnum.ThemeSong:
return 'C';
case EpisodeTypeEnum.Trailer:
return 'T';
case EpisodeTypeEnum.Other:
return 'O';
case EpisodeTypeEnum.Parody:
return 'P';
case EpisodeTypeEnum.Normal:
default:
return '';
}
};

export default getEpisodePrefix;
46 changes: 36 additions & 10 deletions src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { mdiFileDocumentMultipleOutline, mdiLoading, mdiOpenInNew, mdiRefresh }
import { Icon } from '@mdi/react';

import Button from '@/components/Input/Button';
import Checkbox from '@/components/Input/Checkbox';
import ShokoPanel from '@/components/Panels/ShokoPanel';
import ItemCount from '@/components/Utilities/ItemCount';
import EpisodeList from '@/components/Utilities/ReleaseManagement/EpisodeList';
Expand Down Expand Up @@ -57,23 +58,48 @@ const columns: UtilityHeaderType<SeriesWithMultipleReleasesType>[] = [
},
];

const Menu = () => (
<div className="relative box-border flex grow items-center rounded-md border border-panel-border bg-panel-background-alt px-4 py-3">
<MenuButton onClick={() => invalidateQueries(['release-management', 'series'])} icon={mdiRefresh} name="Refresh" />
</div>
);

const MultiplesUtil = () => {
const seriesQuery = useSeriesWithMultipleReleases({ pageSize: 25 });
const [selectedSeries, setSelectedSeries] = useState(0);
const [ignoreVariations, setIgnoreVariations] = useState(true);
const [onlyFinishedSeries, setOnlyFinishedSeries] = useState(true);

const seriesQuery = useSeriesWithMultipleReleases({ ignoreVariations, onlyFinishedSeries, pageSize: 25 });
const [series, seriesCount] = useFlattenListResult(seriesQuery.data);

const [selectedSeries, setSelectedSeries] = useState(0);
const handleCheckboxChange = (type: 'variations' | 'series', checked: boolean) => {
setSelectedSeries(0);
if (type === 'variations') setIgnoreVariations(checked);
if (type === 'series') setOnlyFinishedSeries(checked);
};

return (
<div className="flex grow flex-col gap-y-6 overflow-y-auto">
<ShokoPanel title={<Title />} options={<ItemCount count={seriesCount} series />}>
<div className="flex items-center gap-x-3">
<Menu />
<div className="relative box-border flex grow items-center gap-x-4 rounded-md border border-panel-border bg-panel-background-alt px-4 py-3">
<MenuButton
onClick={() => invalidateQueries(['release-management', 'series'])}
icon={mdiRefresh}
name="Refresh"
/>

<Checkbox
id="ignore-variations"
isChecked={ignoreVariations}
onChange={event => handleCheckboxChange('variations', event.target.checked)}
label="Ignore Variations"
labelRight
/>

<Checkbox
id="only-finished-series"
isChecked={onlyFinishedSeries}
onChange={event => handleCheckboxChange('series', event.target.checked)}
label="Only Finished Series"
labelRight
/>
</div>

<Button buttonType="primary" className="flex gap-x-2.5 px-4 py-3 font-semibold" disabled={seriesCount === 0}>
<Icon path={mdiFileDocumentMultipleOutline} size={0.8333} />
Auto-Delete Multiples
Expand Down Expand Up @@ -109,7 +135,7 @@ const MultiplesUtil = () => {
)}
</div>

<EpisodeList seriesId={selectedSeries} />
<EpisodeList seriesId={selectedSeries} ignoreVariations={ignoreVariations} />
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import React, { useMemo, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { mdiCloseCircleOutline, mdiFileDocumentMultipleOutline, mdiOpenInNew } from '@mdi/js';
import { Icon } from '@mdi/react';
import { countBy, forEach } from 'lodash';
import { countBy, forEach, map, toNumber } from 'lodash';

import FileInfo from '@/components/FileInfo';
import Button from '@/components/Input/Button';
import Select from '@/components/Input/Select';
import ShokoPanel from '@/components/Panels/ShokoPanel';
import toast from '@/components/Toast';
import Title from '@/components/Utilities/ReleaseManagement/Title';
import { useDeleteFileMutation, useMarkVariationMutation } from '@/core/react-query/file/mutations';
import { invalidateQueries } from '@/core/react-query/queryClient';
import getEpisodePrefix from '@/core/utilities/getEpisodePrefix';
import useEventCallback from '@/hooks/useEventCallback';

import type { EpisodeType } from '@/core/types/api/episode';

Expand All @@ -27,6 +32,11 @@ const MultiplesUtilEpisode = () => {
if (!locationState) navigate('../release-management', { replace: true });
const { episode } = locationState;

const { mutateAsync: deleteFile } = useDeleteFileMutation();
const { mutateAsync: markVariation } = useMarkVariationMutation();

const [operationsPending, setOperationsPending] = useState(false);

const [
fileOptions,
setFileOptions,
Expand All @@ -47,6 +57,27 @@ const MultiplesUtilEpisode = () => {

const optionCounts = useMemo(() => countBy(fileOptions), [fileOptions]);

const confirmChanges = useEventCallback(() => {
setOperationsPending(true);

const operations = map(fileOptions, (option, id) => {
const file = episode.Files!.find(item => item.ID === toNumber(id))!;
if (option === 'delete') return deleteFile({ fileId: file.ID, removeFolder: false });
if (option === 'variation' && !file.IsVariation) return markVariation({ fileId: file.ID, variation: true });
if (option === 'keep' && file.IsVariation) return markVariation({ fileId: file.ID, variation: false });
return null;
});

Promise.all(operations)
.then(() => toast.success('Successful!'))
.catch(() => toast.error('One or more operations failed!'))
.finally(() => {
setOperationsPending(false);
invalidateQueries(['release-management', 'series']);
navigate('../release-management', { replace: true });
});
});

return (
<div className="flex grow flex-col gap-y-6 overflow-y-auto">
<ShokoPanel title={<Title />}>
Expand All @@ -55,20 +86,25 @@ const MultiplesUtilEpisode = () => {
<Button
buttonType="secondary"
className="flex gap-x-2.5 px-4 py-3 font-semibold"
onClick={() => navigate('../release-management')}
onClick={() => navigate(-1)}
>
<Icon path={mdiCloseCircleOutline} size={0.8333} />
Cancel
</Button>
<Button buttonType="primary" className="flex gap-x-2.5 px-4 py-3 font-semibold">
<Button
buttonType="primary"
className="flex gap-x-2.5 px-4 py-3 font-semibold"
onClick={confirmChanges}
loading={operationsPending}
>
<Icon path={mdiFileDocumentMultipleOutline} size={0.8333} />
Confirm
</Button>
</div>
</ShokoPanel>
<div className="flex grow flex-col gap-y-6 overflow-y-auto rounded-md border border-panel-border bg-panel-background p-6">
<div className="flex justify-between rounded-lg border border-panel-border bg-panel-table-header p-4 font-semibold">
{`${episode.AniDB?.EpisodeNumber} - ${episode.Name}`}
{`${getEpisodePrefix(episode.AniDB?.Type)}${episode.AniDB?.EpisodeNumber} - ${episode.Name}`}
<div>
<span className="text-panel-text-important">{optionCounts.keep ?? 0}</span>
&nbsp;Kept |&nbsp;
Expand All @@ -88,14 +124,13 @@ const MultiplesUtilEpisode = () => {

<div className="flex flex-col gap-y-4">
<Select
className="flex items-center"
id="mark-variation"
value={fileOptions[file.ID]}
onChange={event => handleOptionChange(event, file.ID)}
>
<option value="keep">Will be kept</option>
<option value="delete">Will be deleted</option>
<option value="variation">Marked as a Variation</option>
<option value="variation">Marked as Variation</option>
</Select>

{file.AniDB?.ID && (
Expand Down

0 comments on commit 4f851b9

Please sign in to comment.