From c1cb562b647fbe13865e31314fe1eff3d5224d3f Mon Sep 17 00:00:00 2001 From: Harshith Mohan <26010946+harshithmohan@users.noreply.github.com> Date: Sat, 1 Aug 2026 10:30:24 +0530 Subject: [PATCH 1/3] Add tooltip option to Checkbox --- src/components/Input/Checkbox.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) 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'} > Date: Sat, 1 Aug 2026 10:39:35 +0530 Subject: [PATCH 2/3] Update empty data handling in release mgmt --- .../ReleaseManagementSeriesDetail.tsx | 72 ++++++++++--------- .../SeriesDetail/CandidatesTab.tsx | 8 +++ .../SeriesDetail/MixAndMatchTab.tsx | 4 +- vite.config.mjs | 2 +- 4 files changed, 51 insertions(+), 35 deletions(-) diff --git a/src/components/Utilities/ReleaseManagement/ReleaseManagementSeriesDetail.tsx b/src/components/Utilities/ReleaseManagement/ReleaseManagementSeriesDetail.tsx index 85ed9864d..fb03997ca 100644 --- a/src/components/Utilities/ReleaseManagement/ReleaseManagementSeriesDetail.tsx +++ b/src/components/Utilities/ReleaseManagement/ReleaseManagementSeriesDetail.tsx @@ -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'; @@ -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'; @@ -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) => { + 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,19 +105,22 @@ const ReleaseManagementSeriesDetail = () => { const [mixMatchSelection, setMixMatchSelection] = useImmer>(new Map()); const [mixMatchUnassignedCount, setMixMatchUnassignedCount] = useState(0); + const seriesQuery = useSeriesQuery(seriesId, {}, seriesId > 0); + const series = seriesQuery.data; + // 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 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]); @@ -128,27 +141,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 +180,9 @@ 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={activeTab === 'candidates' + ? !primaryCandidate + : (seriesDetail?.Overrides.length === 0 || mixMatchUnassignedCount > 0)} > Delete @@ -183,7 +199,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 +212,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 +220,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 +247,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; From 1bfa41dd55276be228c8a96a1ab2aeee6f9ae896 Mon Sep 17 00:00:00 2001 From: Harshith Mohan <26010946+harshithmohan@users.noreply.github.com> Date: Sat, 1 Aug 2026 10:51:35 +0530 Subject: [PATCH 3/3] refactor: extract isDeleteDisabled and update includeVariations comment - Extract delete button disabled logic into named variable with comment - Update includeVariations comment to reflect URL search params behavior --- .../ReleaseManagementSeriesDetail.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/Utilities/ReleaseManagement/ReleaseManagementSeriesDetail.tsx b/src/components/Utilities/ReleaseManagement/ReleaseManagementSeriesDetail.tsx index fb03997ca..4715b22fb 100644 --- a/src/components/Utilities/ReleaseManagement/ReleaseManagementSeriesDetail.tsx +++ b/src/components/Utilities/ReleaseManagement/ReleaseManagementSeriesDetail.tsx @@ -108,9 +108,9 @@ const ReleaseManagementSeriesDetail = () => { const seriesQuery = useSeriesQuery(seriesId, {}, seriesId > 0); const series = seriesQuery.data; - // 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. + // 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, @@ -125,6 +125,13 @@ const ReleaseManagementSeriesDetail = () => { } }, [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 ( <> @@ -180,9 +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 - : (seriesDetail?.Overrides.length === 0 || mixMatchUnassignedCount > 0)} + disabled={isDeleteDisabled} > Delete