Fix selecting migration destination from source browse search page
When going to the source search from the global search, the manga card mode wasn't passed, which prevented a manga from getting selected as the migration destination #1110
This commit is contained in:
@@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
- (**Migration**) Fix search hotkey (ctrl+f) in the single manga migration search page not focusing the search textfield
|
- (**Migration**) Fix search hotkey (ctrl+f) in the single manga migration search page not focusing the search textfield
|
||||||
- (**Migration**) Fix missing manual search option for in progress entry search without a selected match on mobile
|
- (**Migration**) Fix missing manual search option for in progress entry search without a selected match on mobile
|
||||||
- (**Migration**) Fix being unable to retry failed search for a match without a selected match
|
- (**Migration**) Fix being unable to retry failed search for a match without a selected match
|
||||||
|
- (**Migration**) Fix selecting a destination from a source browse search page
|
||||||
- (**Reader**) Fix scrollbar appearing with "fit to widt/height/screen" page scale mode and applied safe area insets
|
- (**Reader**) Fix scrollbar appearing with "fit to widt/height/screen" page scale mode and applied safe area insets
|
||||||
- (**Reader**) Fix wrongly positioned mobile progress bar current page indicator
|
- (**Reader**) Fix wrongly positioned mobile progress bar current page indicator
|
||||||
- (**Reader**) Fix mobile progress bar previous/next chapter button visibility on hover and while disabled
|
- (**Reader**) Fix mobile progress bar previous/next chapter button visibility on hover and while disabled
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { MangaCardMode } from '@/features/manga/Manga.types.ts';
|
||||||
|
|
||||||
export interface RouteStateSourcesSearchAll {
|
export interface RouteStateSourcesSearchAll {
|
||||||
title?: string;
|
title?: string;
|
||||||
shouldShowOnlyPinnedSources?: boolean;
|
shouldShowOnlyPinnedSources?: boolean;
|
||||||
|
mode?: MangaCardMode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { useLingui } from '@lingui/react/macro';
|
|||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { AppbarSearch } from '@/base/components/AppbarSearch.tsx';
|
import { AppbarSearch } from '@/base/components/AppbarSearch.tsx';
|
||||||
import { useDebounce } from '@/base/hooks/useDebounce.ts';
|
import { useDebounce } from '@/base/hooks/useDebounce.ts';
|
||||||
import type { MangaCardProps } from '@/features/manga/Manga.types.ts';
|
import type { MangaCardProps, MangaIdInfo } from '@/features/manga/Manga.types.ts';
|
||||||
import { EmptyView } from '@/base/components/feedback/EmptyView.tsx';
|
import { EmptyView } from '@/base/components/feedback/EmptyView.tsx';
|
||||||
import { STABLE_EMPTY_ARRAY, STABLE_EMPTY_OBJECT } from '@/base/Base.constants.ts';
|
import { STABLE_EMPTY_ARRAY, STABLE_EMPTY_OBJECT } from '@/base/Base.constants.ts';
|
||||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||||
@@ -59,7 +59,6 @@ import { SearchParam } from '@/base/Base.types.ts';
|
|||||||
import { MigrationManager } from '@/features/migration/MigrationManager.ts';
|
import { MigrationManager } from '@/features/migration/MigrationManager.ts';
|
||||||
import { assertIsDefined } from '@/base/Asserts.ts';
|
import { assertIsDefined } from '@/base/Asserts.ts';
|
||||||
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
|
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
|
||||||
import { SubpathUtil } from '@/lib/utils/SubpathUtil.ts';
|
|
||||||
import type { RouteStateSourcesSearchAll } from '@/features/global-search/SearchAll.types.ts';
|
import type { RouteStateSourcesSearchAll } from '@/features/global-search/SearchAll.types.ts';
|
||||||
|
|
||||||
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean; error: any };
|
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean; error: any };
|
||||||
@@ -128,11 +127,13 @@ const SourceSearchPreview = React.memo(
|
|||||||
mode,
|
mode,
|
||||||
shouldShowOnlySourcesWithResults,
|
shouldShowOnlySourcesWithResults,
|
||||||
onMigrateSelect,
|
onMigrateSelect,
|
||||||
|
mangaId,
|
||||||
}: {
|
}: {
|
||||||
source: SourceIdInfo & SourceDisplayNameInfo & SourceNameInfo & SourceLanguageInfo;
|
source: SourceIdInfo & SourceDisplayNameInfo & SourceNameInfo & SourceLanguageInfo;
|
||||||
onSearchRequestFinished: (source: SourceIdInfo, state: SourceLoadingState) => void;
|
onSearchRequestFinished: (source: SourceIdInfo, state: SourceLoadingState) => void;
|
||||||
searchString: string | null | undefined;
|
searchString: string | null | undefined;
|
||||||
emptyQuery: boolean;
|
emptyQuery: boolean;
|
||||||
|
mangaId?: MangaIdInfo['id'];
|
||||||
} & Pick<MangaCardProps, 'mode' | 'onMigrateSelect'> &
|
} & Pick<MangaCardProps, 'mode' | 'onMigrateSelect'> &
|
||||||
Pick<MetadataBrowseSettings, 'shouldShowOnlySourcesWithResults'>) => {
|
Pick<MetadataBrowseSettings, 'shouldShowOnlySourcesWithResults'>) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
@@ -194,6 +195,10 @@ const SourceSearchPreview = React.memo(
|
|||||||
<CardActionArea
|
<CardActionArea
|
||||||
component={Link}
|
component={Link}
|
||||||
to={AppRoutes.sources.children.browse.path(id, searchString)}
|
to={AppRoutes.sources.children.browse.path(id, searchString)}
|
||||||
|
state={AppRoutes.sources.children.browse.state({
|
||||||
|
mode,
|
||||||
|
mangaId,
|
||||||
|
})}
|
||||||
sx={{ p: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
|
sx={{ p: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
@@ -254,8 +259,6 @@ export const SearchAll = ({
|
|||||||
const { state } = useLocation<RouteStateSourcesSearchAll>();
|
const { state } = useLocation<RouteStateSourcesSearchAll>();
|
||||||
const { ref: filterHeaderRef, height: filterHeaderHeight } = useElementSize();
|
const { ref: filterHeaderRef, height: filterHeaderHeight } = useElementSize();
|
||||||
|
|
||||||
const isMigrateMode = SubpathUtil.getPathname().startsWith(AppRoutes.migrate.path);
|
|
||||||
|
|
||||||
const { mangaId } = useParams<{ mangaId?: string }>() ?? STABLE_EMPTY_OBJECT;
|
const { mangaId } = useParams<{ mangaId?: string }>() ?? STABLE_EMPTY_OBJECT;
|
||||||
const [query] = useQueryParam(SearchParam.QUERY, StringParam);
|
const [query] = useQueryParam(SearchParam.QUERY, StringParam);
|
||||||
const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD);
|
const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD);
|
||||||
@@ -281,7 +284,13 @@ export const SearchAll = ({
|
|||||||
const sourceLanguages = useMemo(() => Sources.getLanguages(sources, { excludeLocalSource: true }), [sources]);
|
const sourceLanguages = useMemo(() => Sources.getLanguages(sources, { excludeLocalSource: true }), [sources]);
|
||||||
|
|
||||||
const hasPinnedSources = useMemo(() => !!Sources.filter(sources, { pinned: true }).length, [sources]);
|
const hasPinnedSources = useMemo(() => !!Sources.filter(sources, { pinned: true }).length, [sources]);
|
||||||
const shouldShowOnlyPinnedSources = state?.shouldShowOnlyPinnedSources ?? hasPinnedSources;
|
|
||||||
|
const {
|
||||||
|
title = t`Global Search`,
|
||||||
|
shouldShowOnlyPinnedSources = hasPinnedSources,
|
||||||
|
mode = 'source',
|
||||||
|
} = state ?? STABLE_EMPTY_OBJECT;
|
||||||
|
const isMigrateMode = ['migrate.select.single', 'migrate.select.bulk'].includes(mode);
|
||||||
|
|
||||||
const filteredSources = useMemo(
|
const filteredSources = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -319,7 +328,7 @@ export const SearchAll = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useAppTitleAndAction(
|
useAppTitleAndAction(
|
||||||
isMigrateMode ? state?.title : t`Global Search`,
|
title,
|
||||||
<>
|
<>
|
||||||
<AppbarSearch isClosable={false} />
|
<AppbarSearch isClosable={false} />
|
||||||
<SourceLanguageSelect
|
<SourceLanguageSelect
|
||||||
@@ -429,10 +438,10 @@ export const SearchAll = ({
|
|||||||
onSearchRequestFinished={updateSourceLoadingState}
|
onSearchRequestFinished={updateSourceLoadingState}
|
||||||
searchString={searchString}
|
searchString={searchString}
|
||||||
emptyQuery={!query}
|
emptyQuery={!query}
|
||||||
mode={isMigrateMode ? 'migrate.select' : 'source'}
|
mode={mode}
|
||||||
shouldShowOnlySourcesWithResults={shouldShowOnlySourcesWithResults}
|
shouldShowOnlySourcesWithResults={shouldShowOnlySourcesWithResults}
|
||||||
onMigrateSelect={
|
onMigrateSelect={
|
||||||
migrationDestinationSourceIds
|
isMigrateMode
|
||||||
? (match) => {
|
? (match) => {
|
||||||
assertIsDefined(mangaId);
|
assertIsDefined(mangaId);
|
||||||
MigrationManager.selectManualMatch(Number(mangaId), {
|
MigrationManager.selectManualMatch(Number(mangaId), {
|
||||||
@@ -444,6 +453,7 @@ export const SearchAll = ({
|
|||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
mangaId={mangaId ? Number(mangaId) : undefined}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import type {
|
|||||||
TrackRecordType,
|
TrackRecordType,
|
||||||
} from '@/lib/graphql/generated/graphql-base.types.ts';
|
} from '@/lib/graphql/generated/graphql-base.types.ts';
|
||||||
|
|
||||||
export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate';
|
export type MangaCardMode = 'default' | 'source' | 'migrate.select.bulk' | 'migrate.select.single' | 'duplicate';
|
||||||
|
|
||||||
type MangaCardBaseProps = Pick<MangaTypeGql, 'id' | 'title' | 'sourceId'> &
|
type MangaCardBaseProps = Pick<MangaTypeGql, 'id' | 'title' | 'sourceId'> &
|
||||||
Omit<SingleModeProps['manga'], 'downloadCount' | 'unreadCount' | 'chapters'> &
|
Omit<SingleModeProps['manga'], 'downloadCount' | 'unreadCount' | 'chapters'> &
|
||||||
|
|||||||
@@ -31,21 +31,16 @@ import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler';
|
|||||||
import { MangaMigration } from '@/features/migration/MangaMigration.ts';
|
import { MangaMigration } from '@/features/migration/MangaMigration.ts';
|
||||||
import { MANGA_ACTION_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts';
|
import { MANGA_ACTION_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts';
|
||||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||||
|
import { assertIsDefined } from '@/base/Asserts.ts';
|
||||||
|
|
||||||
const getMangaLinkTo = (
|
const getMangaLinkTo = (mode: MangaCardMode, mangaId: number): string => {
|
||||||
mode: MangaCardMode,
|
|
||||||
mangaId: number,
|
|
||||||
sourceId: string | undefined,
|
|
||||||
mangaTitle: string,
|
|
||||||
): string => {
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'default':
|
case 'default':
|
||||||
case 'source':
|
case 'source':
|
||||||
case 'duplicate':
|
case 'duplicate':
|
||||||
return AppRoutes.manga.path(mangaId);
|
return AppRoutes.manga.path(mangaId);
|
||||||
case 'migrate.search':
|
case 'migrate.select.single':
|
||||||
return AppRoutes.migrate.children.singleMangaSearch.path(sourceId ?? '-1', mangaId, mangaTitle);
|
case 'migrate.select.bulk':
|
||||||
case 'migrate.select':
|
|
||||||
return '';
|
return '';
|
||||||
default:
|
default:
|
||||||
throw new Error(`getMangaLinkTo: unexpected MangaCardMode "${mode}"`);
|
throw new Error(`getMangaLinkTo: unexpected MangaCardMode "${mode}"`);
|
||||||
@@ -73,13 +68,15 @@ export const MangaCard = memo((props: MangaCardProps) => {
|
|||||||
|
|
||||||
const { updateLibraryState, isInLibrary } = useManageMangaLibraryState(manga, mode === 'source');
|
const { updateLibraryState, isInLibrary } = useManageMangaLibraryState(manga, mode === 'source');
|
||||||
|
|
||||||
const mangaLinkTo = getMangaLinkTo(mode, manga.id, manga.sourceId, manga.title);
|
const mangaLinkTo = getMangaLinkTo(mode, manga.id);
|
||||||
|
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(event: React.MouseEvent | React.TouchEvent, openMenu?: () => void) => {
|
(event: React.MouseEvent | React.TouchEvent, openMenu?: () => void) => {
|
||||||
const isDefaultMode = mode === 'default';
|
const isDefaultMode = mode === 'default';
|
||||||
const isSourceMode = mode === 'source';
|
const isSourceMode = mode === 'source';
|
||||||
const isMigrateSelectMode = mode === 'migrate.select';
|
const isMigrationSelectSingleMode = mode === 'migrate.select.single';
|
||||||
|
const isMigrationSelectBulkMode = mode === 'migrate.select.bulk';
|
||||||
|
const isMigrateSelectMode = isMigrationSelectSingleMode || isMigrationSelectBulkMode;
|
||||||
const isSelectionMode = selected !== null;
|
const isSelectionMode = selected !== null;
|
||||||
const isLongPress = !!openMenu;
|
const isLongPress = !!openMenu;
|
||||||
|
|
||||||
@@ -107,8 +104,8 @@ export const MangaCard = memo((props: MangaCardProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMigrateSelectMode) {
|
if (isMigrateSelectMode) {
|
||||||
const isBulkMigrationManualSearch = !!onMigrateSelect;
|
if (isMigrationSelectBulkMode) {
|
||||||
if (isBulkMigrationManualSearch) {
|
assertIsDefined(onMigrateSelect);
|
||||||
onMigrateSelect({ ...manga, missingChapters: undefined });
|
onMigrateSelect({ ...manga, missingChapters: undefined });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ export class MigrationManager {
|
|||||||
{
|
{
|
||||||
state: AppRoutes.migrate.children.singleMangaSearch.state({
|
state: AppRoutes.migrate.children.singleMangaSearch.state({
|
||||||
title: t`Migrate "${manga.title}"`,
|
title: t`Migrate "${manga.title}"`,
|
||||||
|
mode: 'migrate.select.single',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export const MigrationSelectMangas = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BaseMangaGrid
|
<BaseMangaGrid
|
||||||
mode="migrate.select"
|
mode="migrate.select.bulk"
|
||||||
hasNextPage={false}
|
hasNextPage={false}
|
||||||
loadMore={noOp}
|
loadMore={noOp}
|
||||||
isLoading={areMangasLoading}
|
isLoading={areMangasLoading}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type {
|
|||||||
SourcePreferenceChangeInput,
|
SourcePreferenceChangeInput,
|
||||||
SourceType,
|
SourceType,
|
||||||
} from '@/lib/graphql/generated/graphql-base.types.ts';
|
} from '@/lib/graphql/generated/graphql-base.types.ts';
|
||||||
import type { MangaCardMode } from '@/features/manga/Manga.types.ts';
|
import type { MangaCardMode, MangaIdInfo } from '@/features/manga/Manga.types.ts';
|
||||||
|
|
||||||
export interface IPos {
|
export interface IPos {
|
||||||
type: 'selectState' | 'textState' | 'checkBoxState' | 'triState' | 'sortState';
|
type: 'selectState' | 'textState' | 'checkBoxState' | 'triState' | 'sortState';
|
||||||
@@ -86,4 +86,5 @@ export interface RouteStateSourceBrowse {
|
|||||||
contentType?: SourceContentType;
|
contentType?: SourceContentType;
|
||||||
clearCache?: boolean;
|
clearCache?: boolean;
|
||||||
mode?: MangaCardMode;
|
mode?: MangaCardMode;
|
||||||
|
mangaId?: MangaIdInfo['id'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitl
|
|||||||
import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx';
|
import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx';
|
||||||
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
|
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
|
||||||
import { IconWebView } from '@/assets/icons/IconWebView.tsx';
|
import { IconWebView } from '@/assets/icons/IconWebView.tsx';
|
||||||
|
import { MigrationManager } from '@/features/migration/MigrationManager.ts';
|
||||||
|
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
|
||||||
|
|
||||||
const DEFAULT_SOURCE: SourceIdInfo = { id: '-1' };
|
const DEFAULT_SOURCE: SourceIdInfo = { id: '-1' };
|
||||||
|
|
||||||
@@ -216,6 +218,8 @@ export function SourceMangas() {
|
|||||||
const {
|
const {
|
||||||
contentType: initialContentType = SourceContentType.POPULAR,
|
contentType: initialContentType = SourceContentType.POPULAR,
|
||||||
clearCache = false,
|
clearCache = false,
|
||||||
|
mode = 'source',
|
||||||
|
mangaId,
|
||||||
} = useLocation<RouteStateSourceBrowse>().state ?? STABLE_EMPTY_OBJECT;
|
} = useLocation<RouteStateSourceBrowse>().state ?? STABLE_EMPTY_OBJECT;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -485,8 +489,20 @@ export function SourceMangas() {
|
|||||||
messageExtra={messageExtra}
|
messageExtra={messageExtra}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
gridLayout={sourceGridLayout}
|
gridLayout={sourceGridLayout}
|
||||||
mode="source"
|
mode={mode}
|
||||||
inLibraryIndicator
|
inLibraryIndicator
|
||||||
|
onMigrateSelect={
|
||||||
|
mangaId
|
||||||
|
? (match) => {
|
||||||
|
MigrationManager.selectManualMatch(mangaId, {
|
||||||
|
...match,
|
||||||
|
sourceTitle: Sources.getFromCache(match.sourceId)?.displayName,
|
||||||
|
latestChapterNumber: undefined,
|
||||||
|
});
|
||||||
|
ReactRouter.navigate(AppRoutes.migrate.path);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
Reference in New Issue
Block a user