1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05:00

Fix episode status on wanted

This commit is contained in:
Mark McDowall
2025-11-08 11:47:34 -08:00
parent b5967425f1
commit 7960bb8c7d
2 changed files with 263 additions and 223 deletions

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import AppState, { Filter } from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
@@ -22,6 +23,7 @@ import Episode from 'Episode/Episode';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import { align, icons, kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchEpisodeFiles } from 'Store/Actions/episodeFileActions';
import {
batchToggleCutoffUnmetEpisodes,
clearCutoffUnmet,
@@ -35,6 +37,7 @@ import createCommandExecutingSelector from 'Store/Selectors/createCommandExecuti
import { CheckInputChanged } from 'typings/inputs';
import { TableOptionsChangePayload } from 'typings/Table';
import getFilterValue from 'Utilities/Filter/getFilterValue';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import {
registerPagePopulator,
unregisterPagePopulator,
@@ -108,6 +111,14 @@ function CutoffUnmetContent() {
const isSearchingForEpisodes =
isSearchingForAllEpisodes || isSearchingForSelectedEpisodes;
const episodeIds = useMemo(() => {
return selectUniqueIds<Episode, number>(items, 'id');
}, [items]);
const episodeFileIds = useMemo(() => {
return selectUniqueIds<Episode, number>(items, 'episodeFileId');
}, [items]);
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
if (value) {
@@ -214,128 +225,144 @@ function CutoffUnmetContent() {
};
}, [dispatch]);
useEffect(() => {
if (episodeFileIds.length) {
dispatch(fetchEpisodeFiles({ episodeFileIds }));
}
}, [episodeFileIds, dispatch]);
return (
<PageContent title={translate('CutoffUnmet')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={
anySelected ? translate('SearchSelected') : translate('SearchAll')
}
iconName={icons.SEARCH}
isDisabled={isSearchingForEpisodes}
isSpinning={isSearchingForEpisodes}
onPress={
anySelected ? handleSearchSelectedPress : handleSearchAllPress
}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={
isShowingMonitored
? translate('UnmonitorSelected')
: translate('MonitorSelected')
}
iconName={icons.MONITORED}
isDisabled={!anySelected}
isSpinning={isSaving}
onPress={handleToggleSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<QueueDetailsProvider episodeIds={episodeIds}>
<PageContent title={translate('CutoffUnmet')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
label={
anySelected
? translate('SearchSelected')
: translate('SearchAll')
}
iconName={icons.SEARCH}
isDisabled={isSearchingForEpisodes}
isSpinning={isSearchingForEpisodes}
onPress={
anySelected ? handleSearchSelectedPress : handleSearchAllPress
}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageToolbarSeparator />
<PageContentBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
<PageToolbarButton
label={
isShowingMonitored
? translate('UnmonitorSelected')
: translate('MonitorSelected')
}
iconName={icons.MONITORED}
isDisabled={!anySelected}
isSpinning={isSaving}
onPress={handleToggleSelectedPress}
/>
</PageToolbarSection>
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('CutoffUnmetLoadError')}</Alert>
) : null}
{isPopulated && !error && !items.length ? (
<Alert kind={kinds.INFO}>{translate('CutoffUnmetNoItems')}</Alert>
) : null}
{isPopulated && !error && !!items.length ? (
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<CutoffUnmetRow key={item.id} columns={columns} {...item} />
);
})}
</TableBody>
</Table>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<ConfirmModal
isOpen={isConfirmSearchAllModalOpen}
kind={kinds.DANGER}
title={translate('SearchForCutoffUnmetEpisodes')}
message={
<div>
<PageContentBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>
{translate('CutoffUnmetLoadError')}
</Alert>
) : null}
{isPopulated && !error && !items.length ? (
<Alert kind={kinds.INFO}>{translate('CutoffUnmetNoItems')}</Alert>
) : null}
{isPopulated && !error && !!items.length ? (
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<CutoffUnmetRow
key={item.id}
columns={columns}
{...item}
/>
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
<ConfirmModal
isOpen={isConfirmSearchAllModalOpen}
kind={kinds.DANGER}
title={translate('SearchForCutoffUnmetEpisodes')}
message={
<div>
{translate(
'SearchForCutoffUnmetEpisodesConfirmationCount',
{ totalRecords }
)}
<div>
{translate(
'SearchForCutoffUnmetEpisodesConfirmationCount',
{ totalRecords }
)}
</div>
<div>{translate('MassSearchCancelWarning')}</div>
</div>
<div>{translate('MassSearchCancelWarning')}</div>
</div>
}
confirmLabel={translate('Search')}
onConfirm={handleSearchAllCutoffUnmetConfirmed}
onCancel={handleConfirmSearchAllCutoffUnmetModalClose}
/>
</div>
) : null}
</PageContentBody>
</PageContent>
}
confirmLabel={translate('Search')}
onConfirm={handleSearchAllCutoffUnmetConfirmed}
onCancel={handleConfirmSearchAllCutoffUnmetModalClose}
/>
</div>
) : null}
</PageContentBody>
</PageContent>
</QueueDetailsProvider>
);
}

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import AppState, { Filter } from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
@@ -36,6 +37,7 @@ import createCommandExecutingSelector from 'Store/Selectors/createCommandExecuti
import { CheckInputChanged } from 'typings/inputs';
import { TableOptionsChangePayload } from 'typings/Table';
import getFilterValue from 'Utilities/Filter/getFilterValue';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import {
registerPagePopulator,
unregisterPagePopulator,
@@ -112,6 +114,10 @@ function MissingContent() {
const isSearchingForEpisodes =
isSearchingForAllEpisodes || isSearchingForSelectedEpisodes;
const episodeIds = useMemo(() => {
return selectUniqueIds<Episode, number>(items, 'id');
}, [items]);
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
if (value) {
@@ -227,139 +233,146 @@ function MissingContent() {
}, [dispatch]);
return (
<PageContent title={translate('Missing')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={
anySelected ? translate('SearchSelected') : translate('SearchAll')
}
iconName={icons.SEARCH}
isDisabled={isSearchingForEpisodes}
isSpinning={isSearchingForEpisodes}
onPress={
anySelected ? handleSearchSelectedPress : handleSearchAllPress
}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={
isShowingMonitored
? translate('UnmonitorSelected')
: translate('MonitorSelected')
}
iconName={icons.MONITORED}
isDisabled={!anySelected}
isSpinning={isSaving}
onPress={handleToggleSelectedPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('ManualImport')}
iconName={icons.INTERACTIVE}
onPress={handleInteractiveImportPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<QueueDetailsProvider episodeIds={episodeIds}>
<PageContent title={translate('Missing')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
label={
anySelected
? translate('SearchSelected')
: translate('SearchAll')
}
iconName={icons.SEARCH}
isDisabled={isSearchingForEpisodes}
isSpinning={isSearchingForEpisodes}
onPress={
anySelected ? handleSearchSelectedPress : handleSearchAllPress
}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageToolbarSeparator />
<PageContentBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
<PageToolbarButton
label={
isShowingMonitored
? translate('UnmonitorSelected')
: translate('MonitorSelected')
}
iconName={icons.MONITORED}
isDisabled={!anySelected}
isSpinning={isSaving}
onPress={handleToggleSelectedPress}
/>
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('MissingLoadError')}</Alert>
) : null}
<PageToolbarSeparator />
{isPopulated && !error && !items.length ? (
<Alert kind={kinds.INFO}>{translate('MissingNoItems')}</Alert>
) : null}
<PageToolbarButton
label={translate('ManualImport')}
iconName={icons.INTERACTIVE}
onPress={handleInteractiveImportPress}
/>
</PageToolbarSection>
{isPopulated && !error && !!items.length ? (
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<MissingRow key={item.id} columns={columns} {...item} />
);
})}
</TableBody>
</Table>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<ConfirmModal
isOpen={isConfirmSearchAllModalOpen}
kind={kinds.DANGER}
title={translate('SearchForAllMissingEpisodes')}
message={
<div>
<PageContentBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('MissingLoadError')}</Alert>
) : null}
{isPopulated && !error && !items.length ? (
<Alert kind={kinds.INFO}>{translate('MissingNoItems')}</Alert>
) : null}
{isPopulated && !error && !!items.length ? (
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<MissingRow key={item.id} columns={columns} {...item} />
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
<ConfirmModal
isOpen={isConfirmSearchAllModalOpen}
kind={kinds.DANGER}
title={translate('SearchForAllMissingEpisodes')}
message={
<div>
{translate('SearchForAllMissingEpisodesConfirmationCount', {
totalRecords,
})}
<div>
{translate(
'SearchForAllMissingEpisodesConfirmationCount',
{
totalRecords,
}
)}
</div>
<div>{translate('MassSearchCancelWarning')}</div>
</div>
<div>{translate('MassSearchCancelWarning')}</div>
</div>
}
confirmLabel={translate('Search')}
onConfirm={handleSearchAllMissingConfirmed}
onCancel={handleConfirmSearchAllMissingModalClose}
/>
</div>
) : null}
</PageContentBody>
}
confirmLabel={translate('Search')}
onConfirm={handleSearchAllMissingConfirmed}
onCancel={handleConfirmSearchAllMissingModalClose}
/>
</div>
) : null}
</PageContentBody>
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
onModalClose={handleInteractiveImportModalClose}
/>
</PageContent>
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
onModalClose={handleInteractiveImportModalClose}
/>
</PageContent>
</QueueDetailsProvider>
);
}