Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/components/Input/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import type { ChangeEventHandler, ReactNode } from 'react';
import type { PlacesType } from 'react-tooltip';
import { mdiCheckboxBlankCircleOutline, mdiCheckboxMarkedCircleOutline } from '@mdi/js';
import { Icon } from '@mdi/react';
import cx from 'classnames';
Expand All @@ -19,6 +20,8 @@ type Props = {
labelRight?: boolean;
justify?: boolean;
onChange: ChangeEventHandler<HTMLInputElement>;
tooltip?: string;
tooltipPlace?: PlacesType;
};

const Checkbox = (props: Props) => {
Expand All @@ -33,6 +36,8 @@ const Checkbox = (props: Props) => {
labelClassName,
labelRight,
onChange,
tooltip,
tooltipPlace,
} = props;
const bodyVisible = useBodyVisibleContext();
const inputRef = useAutoFocusRef(autoFocus && !disabled && bodyVisible);
Expand All @@ -50,6 +55,9 @@ const Checkbox = (props: Props) => {
disabled ? 'cursor-auto' : 'cursor-pointer',
'h-8',
])}
data-tooltip-id="tooltip"
data-tooltip-content={tooltip}
data-tooltip-place={tooltipPlace ?? 'top'}
>
<input
id={id}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import type { ChangeEvent } from 'react';
import { Link, useParams, useSearchParams } from 'react-router';
import { mdiChevronRight, mdiFlagOutline, mdiLoading, mdiOpenInNew, mdiTrashCanOutline } from '@mdi/js';
import { Icon } from '@mdi/react';
Expand All @@ -14,6 +15,7 @@ import ShokoPanel from '@/components/Panels/ShokoPanel';
import ShokoIcon from '@/components/ShokoIcon';
import ReleaseManagementPreviewModal from '@/components/Utilities/ReleaseManagement/ReleaseManagementPreviewModal';
import { useReleaseManagementSeriesDetailQuery } from '@/core/react-query/release-management/queries';
import { useSeriesQuery } from '@/core/react-query/series/queries';
import toast from '@/core/toast';
import { getAnidbAnimeLink } from '@/core/util';
import useNavigateVoid from '@/hooks/useNavigateVoid';
Expand Down Expand Up @@ -82,11 +84,19 @@ const Title = (
const ReleaseManagementSeriesDetail = () => {
const navigate = useNavigateVoid();
const { seriesId: seriesIdParam } = useParams<{ seriesId: string }>();
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();

const seriesId = toNumber(seriesIdParam ?? 0);
const activeTab = searchParams.get('tab') ?? 'candidates';
const includeVariations = (searchParams.get('includeVariations') ?? 'true') === 'true';
const includeVariations = activeTab === 'mixmatch' || (searchParams.get('includeVariations') ?? 'true') === 'true';

const handleFilterChange = (event: ChangeEvent<HTMLInputElement>) => {
setSearchParams((currentParams) => {
const newParams = new URLSearchParams(currentParams);
newParams.set(event.target.id, String(event.target.checked));
return newParams;
});
};

const [showManageVariationsModal, toggleManageVariationsModal] = useToggle(false);
const [showPreviewModal, togglePreviewModal] = useToggle(false);
Expand All @@ -95,23 +105,33 @@ const ReleaseManagementSeriesDetail = () => {
const [mixMatchSelection, setMixMatchSelection] = useImmer<Map<string, number>>(new Map());
const [mixMatchUnassignedCount, setMixMatchUnassignedCount] = useState(0);

// Candidates tab passes through the list page's includeVariations toggle, same as the batch
// preview. Mix & Match always wants every file - variations included - as a pickable option
// regardless of that toggle, so it fetches independently with includeVariations forced true.
const seriesQuery = useReleaseManagementSeriesDetailQuery(
const seriesQuery = useSeriesQuery(seriesId, {}, seriesId > 0);
const series = seriesQuery.data;

// The includeVariations toggle is managed via URL search params and is functional when on the
// Candidates tab. Mix & Match always wants every file - variations included - as a pickable
// option regardless of that toggle, so it fetches independently with includeVariations forced true.
const seriesDetailQuery = useReleaseManagementSeriesDetailQuery(
seriesId,
activeTab === 'mixmatch' ? true : includeVariations,
seriesId > 0,
);
const series = seriesQuery.data;
const seriesDetail = seriesDetailQuery.data;

useEffect(() => {
if (seriesQuery.isError) {
toast.error(`Series ${seriesId} is invalid or does not have multiple releases`);
toast.error(`Series ${seriesId} is invalid`);
navigate('/webui/utilities/release-management');
}
}, [navigate, seriesId, seriesQuery.isError]);

// Delete is disabled when:
// - Candidates tab: no primary candidate selected
// - Mix & Match tab: no overrides exist, or there are unassigned episodes
const isDeleteDisabled = activeTab === 'candidates'
? !primaryCandidate
: ((seriesDetail?.Overrides.length ?? 0) === 0 || mixMatchUnassignedCount > 0);

if (seriesQuery.isPending || !series) {
return (
<>
Expand All @@ -128,27 +148,28 @@ const ReleaseManagementSeriesDetail = () => {
return (
<>
<title>
{`${series.SeriesTitle} | Release Management | Shoko`}
{`${series.Name} | Release Management | Shoko`}
</title>
<ShokoPanel
title={
<Title
seriesTitle={series.SeriesTitle}
seriesId={series.SeriesID}
anidbId={series.AnidbAnimeID}
isAiring={series.IsAiring}
seriesTitle={series.Name}
seriesId={series.IDs.ID}
anidbId={series.IDs.AniDB}
isAiring={seriesDetail?.IsAiring ?? false}
/>
}
>
<div className="flex items-center gap-x-3">
<div className="flex grow items-center gap-x-4 rounded-md border border-panel-border bg-panel-background-alt px-4 py-2">
<div className="flex min-h-13 grow items-center gap-x-4 rounded-md border border-panel-border bg-panel-background-alt px-4 py-2">
<Checkbox
id="includeVariations"
isChecked={includeVariations}
onChange={() => ({})}
onChange={handleFilterChange}
label="Include Variations"
labelRight
disabled
disabled={activeTab === 'mixmatch'}
tooltip={activeTab === 'mixmatch' ? 'Variations are always included for Mix & Match' : ''}
/>
</div>

Expand All @@ -166,7 +187,7 @@ const ReleaseManagementSeriesDetail = () => {
buttonType="danger"
className="flex items-center gap-x-2.5 px-4 py-3 font-semibold whitespace-nowrap"
onClick={togglePreviewModal}
disabled={activeTab === 'candidates' ? !primaryCandidate : mixMatchUnassignedCount > 0}
disabled={isDeleteDisabled}
>
<Icon path={mdiTrashCanOutline} size={0.8333} />
Delete
Expand All @@ -183,7 +204,7 @@ const ReleaseManagementSeriesDetail = () => {
? 'border-b-2 border-panel-text-primary text-panel-text-primary'
: 'opacity-65 hover:opacity-100',
)}
to="?tab=candidates"
to={`?tab=candidates&includeVariations=${includeVariations}`}
replace
>
Candidates
Expand All @@ -196,40 +217,32 @@ const ReleaseManagementSeriesDetail = () => {
? 'border-b-2 border-panel-text-primary text-panel-text-primary'
: 'opacity-65 hover:opacity-100',
)}
to="?tab=mixmatch"
to={`?tab=mixmatch&includeVariations=${includeVariations}`}
replace
>
Mix &amp; Match
</Link>
</div>

{/* Content */}
{seriesQuery.isPending && (
<div className="flex h-32 items-center justify-center text-panel-text-primary">
<Icon path={mdiLoading} size={2} spin />
</div>
)}

{seriesQuery.isError && (
<div className="flex h-32 items-center justify-center">
<span className="text-panel-text-danger">
Failed to load series candidates. The series may have no multiple releases.
</span>
{seriesDetailQuery.isPending && (
<div className="flex grow items-center justify-center text-panel-text-primary">
<Icon path={mdiLoading} size={4} spin />
</div>
)}

{series && activeTab === 'candidates' && (
{seriesDetail && activeTab === 'candidates' && (
<CandidatesTab
primaryCandidate={primaryCandidate}
series={series}
series={seriesDetail}
setPrimaryCandidate={setPrimaryCandidate}
/>
)}

{series && activeTab === 'mixmatch' && (
{seriesDetail && activeTab === 'mixmatch' && (
<MixAndMatchTab
selection={mixMatchSelection}
series={series}
series={seriesDetail}
setSelection={setMixMatchSelection}
setUnassignedCount={setMixMatchUnassignedCount}
/>
Expand All @@ -239,7 +252,7 @@ const ReleaseManagementSeriesDetail = () => {
<ManageVariationsModal
show={showManageVariationsModal}
seriesId={seriesId}
seriesTitle={series?.SeriesTitle}
seriesTitle={seriesDetail?.SeriesTitle}
onClose={toggleManageVariationsModal}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ const CandidatesTab = ({ primaryCandidate, series, setPrimaryCandidate }: Props)

const allCandidatesLackReleaseInfo = series.Candidates.every(candidate => !candidate.HasReleaseInfo);

if (series.Candidates.length === 0) {
return (
<div className="flex grow items-center justify-center">
The series does not have any candidates
</div>
);
}

return (
<div className="flex grow flex-col gap-4">
{allCandidatesLackReleaseInfo && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ const MixAndMatchTab = ({ selection, series, setSelection, setUnassignedCount }:

if (allEpisodes.length === 0) {
return (
<div className="flex h-48 items-center justify-center">
No episodes found across candidates.
<div className="flex grow items-center justify-center">
The series does not have any mix & match options
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion vite.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default defineConfig(async () => {
async function setupEnv(isDebug) {
const gitHash = childProcess.execSync("git log --pretty=format:'%h' -n 1").toString().replace(/["']/g, '');
const appVersion = pkg.version;
const minimumServerVersion = '6.0.0-dev.389';
const minimumServerVersion = '6.0.0-dev.400';

process.env.VITE_GITHASH = gitHash;
process.env.VITE_APPVERSION = appVersion;
Expand Down