diff --git a/src/components/Input/Checkbox.tsx b/src/components/Input/Checkbox.tsx index 98676b1ed..8bcaf9989 100644 --- a/src/components/Input/Checkbox.tsx +++ b/src/components/Input/Checkbox.tsx @@ -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'; @@ -19,6 +20,8 @@ type Props = { labelRight?: boolean; justify?: boolean; onChange: ChangeEventHandler; + tooltip?: string; + tooltipPlace?: PlacesType; }; const Checkbox = (props: Props) => { @@ -33,6 +36,8 @@ const Checkbox = (props: Props) => { labelClassName, labelRight, onChange, + tooltip, + tooltipPlace, } = props; const bodyVisible = useBodyVisibleContext(); const inputRef = useAutoFocusRef(autoFocus && !disabled && bodyVisible); @@ -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'} > { 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) => { + 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); @@ -95,23 +105,33 @@ const ReleaseManagementSeriesDetail = () => { const [mixMatchSelection, setMixMatchSelection] = useImmer>(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 ( <> @@ -128,27 +148,28 @@ const ReleaseManagementSeriesDetail = () => { return ( <> - {`${series.SeriesTitle} | Release Management | Shoko`} + {`${series.Name} | Release Management | Shoko`} } >
-
+
({})} + onChange={handleFilterChange} label="Include Variations" labelRight - disabled + disabled={activeTab === 'mixmatch'} + tooltip={activeTab === 'mixmatch' ? 'Variations are always included for Mix & Match' : ''} />
@@ -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} > Delete @@ -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 @@ -196,7 +217,7 @@ 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 & Match @@ -204,32 +225,24 @@ const ReleaseManagementSeriesDetail = () => {
{/* Content */} - {seriesQuery.isPending && ( -
- -
- )} - - {seriesQuery.isError && ( -
- - Failed to load series candidates. The series may have no multiple releases. - + {seriesDetailQuery.isPending && ( +
+
)} - {series && activeTab === 'candidates' && ( + {seriesDetail && activeTab === 'candidates' && ( )} - {series && activeTab === 'mixmatch' && ( + {seriesDetail && activeTab === 'mixmatch' && ( @@ -239,7 +252,7 @@ const ReleaseManagementSeriesDetail = () => { diff --git a/src/components/Utilities/ReleaseManagement/SeriesDetail/CandidatesTab.tsx b/src/components/Utilities/ReleaseManagement/SeriesDetail/CandidatesTab.tsx index 645ff84a7..7cf382e0f 100644 --- a/src/components/Utilities/ReleaseManagement/SeriesDetail/CandidatesTab.tsx +++ b/src/components/Utilities/ReleaseManagement/SeriesDetail/CandidatesTab.tsx @@ -69,6 +69,14 @@ const CandidatesTab = ({ primaryCandidate, series, setPrimaryCandidate }: Props) const allCandidatesLackReleaseInfo = series.Candidates.every(candidate => !candidate.HasReleaseInfo); + if (series.Candidates.length === 0) { + return ( +
+ The series does not have any candidates +
+ ); + } + return (
{allCandidatesLackReleaseInfo && ( diff --git a/src/components/Utilities/ReleaseManagement/SeriesDetail/MixAndMatchTab.tsx b/src/components/Utilities/ReleaseManagement/SeriesDetail/MixAndMatchTab.tsx index 0c174191a..7abaf6c00 100644 --- a/src/components/Utilities/ReleaseManagement/SeriesDetail/MixAndMatchTab.tsx +++ b/src/components/Utilities/ReleaseManagement/SeriesDetail/MixAndMatchTab.tsx @@ -160,8 +160,8 @@ const MixAndMatchTab = ({ selection, series, setSelection, setUnassignedCount }: if (allEpisodes.length === 0) { return ( -
- No episodes found across candidates. +
+ The series does not have any mix & match options
); } diff --git a/vite.config.mjs b/vite.config.mjs index 955a5337a..a31fd8289 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -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;