Skip to content

Commit

Permalink
Add search to collection (#708)
Browse files Browse the repository at this point in the history
* Add search to collection

* Use `useDebounce` wherever applicable

* Add 'Infinite' suffix to paginated merged queries
  • Loading branch information
harshithmohan authored Dec 5, 2023
1 parent 494cc87 commit 6a11b5d
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 95 deletions.
9 changes: 8 additions & 1 deletion src/components/Collection/CollectionTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import cx from 'classnames';
type Props = {
count: number;
filterOrGroup?: string;
searchQuery: string;
};

const CollectionTitle = ({ count, filterOrGroup }: Props) => (
const CollectionTitle = ({ count, filterOrGroup, searchQuery }: Props) => (
<div className="flex items-center gap-x-2 text-xl font-semibold">
<Link to="/webui/collection" className={cx(filterOrGroup ? 'text-panel-text-primary' : 'pointer-events-none')}>
Entire Collection
Expand All @@ -20,6 +21,12 @@ const CollectionTitle = ({ count, filterOrGroup }: Props) => (
{filterOrGroup}
</>
)}
{searchQuery && (
<>
<Icon path={mdiChevronRight} size={1} />
Search Results
</>
)}
<span>|</span>
<span className="text-panel-text-important">
{/* Count is set to -1 when series data is empty and is used as a flag to signify that in other places */}
Expand Down
71 changes: 61 additions & 10 deletions src/components/Collection/CollectionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,23 @@ import { debounce } from 'lodash';

import ListViewItem from '@/components/Collection/ListViewItem';
import PosterViewItem from '@/components/Collection/PosterViewItem';
import { useLazyGetGroupSeriesQuery, useLazyGetGroupsQuery } from '@/core/rtkQuery/splitV3Api/collectionApi';
import buildFilter from '@/core/buildFilter';
import { useLazyGetGroupSeriesQuery } from '@/core/rtkQuery/splitV3Api/collectionApi';
import { useGetFilterQuery, useLazyGetFilteredGroupsInfiniteQuery } from '@/core/rtkQuery/splitV3Api/filterApi';
import { useGetSettingsQuery } from '@/core/rtkQuery/splitV3Api/settingsApi';
import { useLazyGetGroupViewQuery } from '@/core/rtkQuery/splitV3Api/webuiApi';
import { useLazyGetGroupViewInfiniteQuery } from '@/core/rtkQuery/splitV3Api/webuiApi';
import { initialSettings } from '@/pages/settings/SettingsPage';

import type { InfiniteResultType } from '@/core/types/api';
import type { FilterCondition, FilterType } from '@/core/types/api/filter';
import type { SeriesType } from '@/core/types/api/series';

type Props = {
mode: string;
setGroupTotal: (total: number) => void;
setTimelineSeries: (series: SeriesType[]) => void;
isSidebarOpen: boolean;
searchQuery: string;
};

const defaultPageSize = 50;
Expand All @@ -40,10 +44,40 @@ export const listItemSize = {
gap: 32,
};

const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries }: Props) => {
const getFilter = (query: string, filterCondition?: FilterCondition): FilterType => {
let finalCondition: FilterCondition | undefined;
if (query) {
const searchCondition: FilterCondition = {
Type: 'StringFuzzyMatches',
Left: {
Type: 'NameSelector',
},
Parameter: query,
};

if (filterCondition) {
finalCondition = buildFilter([searchCondition, filterCondition]);
} else {
finalCondition = buildFilter([searchCondition]);
}
} else if (filterCondition) {
finalCondition = buildFilter([filterCondition]);
}

return (
finalCondition
? {
Expression: finalCondition,
}
: {}
);
};

const CollectionView = ({ isSidebarOpen, mode, searchQuery, setGroupTotal, setTimelineSeries }: Props) => {
const { filterId, groupId } = useParams();

const [currentFilterId, setCurrentFilterId] = useState(filterId);
const [currentSearch, setCurrentSearch] = useState(searchQuery);

const settingsQuery = useGetSettingsQuery();
const settings = useMemo(() => settingsQuery?.data ?? initialSettings, [settingsQuery]);
Expand All @@ -54,6 +88,8 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
[mode, showRandomPosterGrid, showRandomPosterList],
);

const filterQuery = useGetFilterQuery({ filterId: filterId! }, { skip: !filterId });

const [itemWidth, itemHeight, itemGap] = useMemo(() => {
if (mode === 'poster') return [posterItemSize.width, posterItemSize.height, posterItemSize.gap];
return [
Expand All @@ -65,7 +101,7 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries

const [fetchingPage, setFetchingPage] = useState(false);

const [fetchGroups, groupsData] = useLazyGetGroupsQuery();
const [fetchGroups, groupsData] = useLazyGetFilteredGroupsInfiniteQuery();
const [fetchSeries, seriesDataResult] = useLazyGetGroupSeriesQuery();
const [seriesData, setSeriesData] = useState<InfiniteResultType<SeriesType[]>>({ pages: [], total: -1 });
// This is to set an extra arg for groupsQuery so that cache is invalidated correctly. Using state because this should not change once component is mounted.
Expand All @@ -79,7 +115,7 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
[groupId, groupsData, seriesData],
);

const [fetchGroupExtras, groupExtrasData] = useLazyGetGroupViewQuery();
const [fetchGroupExtras, groupExtrasData] = useLazyGetGroupViewInfiniteQuery();
const groupExtras = groupExtrasData.data ?? [];

useEffect(() => {
Expand All @@ -100,11 +136,13 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
fetchGroups({
page,
pageSize,
filterId: filterId ?? '0',
randomImages: showRandomPoster,
filterCriteria: getFilter(searchQuery, filterId ? filterQuery.data?.Expression : undefined),
queryId: groupQueryId,
}).then(
(result) => {
setCurrentFilterId(filterId);

if (!result.data) return;

const ids = result.data.pages[page].map(group => group.IDs.ID);
Expand All @@ -120,7 +158,18 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
.then(result => result.data && setSeriesData(result.data))
.catch(error => console.error(error)).finally(() => setFetchingPage(false));
}
}, 200), [groupId, fetchGroups, pageSize, filterId, showRandomPoster, groupQueryId, fetchGroupExtras, fetchSeries]);
}, 200), [
groupId,
fetchGroups,
pageSize,
showRandomPoster,
searchQuery,
filterId,
filterQuery.data?.Expression,
groupQueryId,
fetchGroupExtras,
fetchSeries,
]);

useEffect(() => {
fetchPage.cancel();
Expand All @@ -129,8 +178,10 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
let shouldFetch: boolean;
if (groupId) {
shouldFetch = true;
} else if (searchQuery !== currentSearch) {
setCurrentSearch(searchQuery);
shouldFetch = true;
} else if (filterId !== currentFilterId) {
setCurrentFilterId(filterId);
shouldFetch = true;
} else {
shouldFetch = groupsData.isUninitialized;
Expand All @@ -143,7 +194,7 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
return () => fetchPage.cancel();
// TODO: Figure out how to do it better
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterId, groupId, groupsData.isUninitialized, fetchPage]);
}, [filterId, groupId, groupsData.isUninitialized, fetchPage, searchQuery]);

useEffect(() => {
if (!groupId) setSeriesData({ pages: [], total: -1 });
Expand Down Expand Up @@ -181,7 +232,7 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
{/* This is always equal width to the actual grid container so we are using the ref here */}
{/* Otherwise we would need two refs to remove flicker */}
<div className="flex w-full justify-center" ref={gridContainerRef}>
{isLoading || seriesData.total === -1
{isLoading || (groupId && seriesData.total === -1)
? <Icon path={mdiLoading} size={3} className="text-panel-text-primary" spin />
: 'No series/groups available!'}
</div>
Expand Down
62 changes: 18 additions & 44 deletions src/components/Utilities/Unrecognized/AvDumpSeriesSelectModal.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { mdiInformationOutline, mdiLoading, mdiMagnify, mdiOpenInNew } from '@mdi/js';
import { Icon } from '@mdi/react';
import { debounce, forEach } from 'lodash';
import { useEventCallback } from 'usehooks-ts';
import { forEach } from 'lodash';
import { useDebounce, useEventCallback } from 'usehooks-ts';

import Button from '@/components/Input/Button';
import Input from '@/components/Input/Input';
import ModalPanel from '@/components/Panels/ModalPanel';
import toast from '@/components/Toast';
import { usePostFileRescanMutation } from '@/core/rtkQuery/splitV3Api/fileApi';
import { useLazyGetSeriesAniDBSearchQuery } from '@/core/rtkQuery/splitV3Api/seriesApi';
import { useGetSeriesAniDBSearchQuery } from '@/core/rtkQuery/splitV3Api/seriesApi';
import { copyToClipboard } from '@/core/util';
import { detectShow, findMostCommonShowName } from '@/core/utilities/auto-match-logic';

Expand Down Expand Up @@ -62,24 +62,19 @@ function AvDumpSeriesSelectModal({ getLinks, onClose, show }: Props) {
});
return { ed2kLinks: tempEd2kLinks, links: tempLinks, fileIds: tempFileIds };
}, [getLinks, show]);
const commonSeries = useMemo(() => findMostCommonShowName(links.map(link => detectShow(link.split('|')[2]))), [
links,
]);
const initRef = useRef(false);
const [searchText, setSearchText] = useState(() => commonSeries);
const [searchTrigger, searchResults] = useLazyGetSeriesAniDBSearchQuery();
const commonSeries = useMemo(
() => findMostCommonShowName(links.map(link => detectShow(link.split('|')[2]))),
[links],
);
const [searchText, setSearchText] = useState('');
const [activeStep, setActiveStep] = useState(1);
const [copyFailed, setCopyFailed] = useState(false);
const debouncedSearch = useDebounce(searchText, 200);
const searchQuery = useGetSeriesAniDBSearchQuery({ query: debouncedSearch }, { skip: !debouncedSearch });

const debouncedSearch = useRef(
debounce((query: string) => {
searchTrigger({ query, pageSize: 20 }).catch(() => {});
}, 200),
).current;

const handleClose = () => {
onClose(false);
};
useEffect(() => {
setSearchText(commonSeries);
}, [commonSeries]);

const handleNextStep = () => {
setActiveStep(activeStep + 1);
Expand All @@ -90,18 +85,6 @@ function AvDumpSeriesSelectModal({ getLinks, onClose, show }: Props) {
setActiveStep(activeStep - 1);
};

const handleSearch = useEventCallback((query: string) => {
setSearchText(query);
if (query !== '') {
if (initRef.current) {
initRef.current = false;
searchTrigger({ query, pageSize: 20 }).catch(() => {});
} else {
debouncedSearch(query);
}
}
});

const handleCopy = () => {
copyToClipboard(ed2kLinks)
.then(() => {
Expand Down Expand Up @@ -129,17 +112,8 @@ function AvDumpSeriesSelectModal({ getLinks, onClose, show }: Props) {
if (failedFiles !== fileIds.length) toast.success(`Rescanning ${fileIds.length} files!`);
});

useEffect(() => () => {
debouncedSearch.cancel();
}, [debouncedSearch]);

useEffect(() => {
handleSearch(commonSeries);
}, [commonSeries, handleSearch]);

useLayoutEffect(() => () => {
if (show) return;
initRef.current = true;
setSearchText('');
setClickedLink(false);
setCopyFailed(false);
Expand Down Expand Up @@ -178,7 +152,7 @@ function AvDumpSeriesSelectModal({ getLinks, onClose, show }: Props) {
<Button
buttonType="secondary"
className="flex items-center justify-center px-4 py-2"
onClick={handleClose}
onClick={() => onClose(false)}
>
Cancel
</Button>
Expand Down Expand Up @@ -216,18 +190,18 @@ function AvDumpSeriesSelectModal({ getLinks, onClose, show }: Props) {
value={searchText}
type="text"
placeholder="Search..."
onChange={e => handleSearch(e.target.value)}
onChange={e => setSearchText(e.target.value)}
startIcon={mdiMagnify}
/>
<div className="w-full rounded-md border border-panel-border bg-panel-input p-4 capitalize">
<div className="shoko-scrollbar flex h-[9.5rem] flex-col gap-y-1 overflow-x-clip overflow-y-scroll rounded-md bg-panel-input pr-2 ">
{initRef.current || searchResults.isError || searchResults.isFetching
{searchQuery.isError || searchQuery.isFetching
? (
<div className="flex h-full items-center justify-center">
<Icon path={mdiLoading} size={3} spin className="text-panel-text-primary" />
</div>
)
: (searchResults.data ?? []).map(result => (
: (searchQuery.data ?? []).map(result => (
<a
href={`https://anidb.net/anime/${result.ID}/release/add`}
key={result.ID}
Expand Down
14 changes: 14 additions & 0 deletions src/core/buildFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { FilterCondition } from '@/core/types/api/filter';

const buildFilter = (filters: FilterCondition[]) => {
if (filters.length > 1) {
return {
Type: 'And',
Left: filters[0],
Right: buildFilter(filters.slice(1)),
};
}
return filters[0];
};

export default buildFilter;
8 changes: 2 additions & 6 deletions src/core/rtkQuery/splitV3Api/collectionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { SeriesType } from '@/core/types/api/series';

const collectionApi = splitV3Api.injectEndpoints({
endpoints: build => ({
getGroups: build.query<
getGroupsInfinite: build.query<
InfiniteResultType<CollectionGroupType[]>,
PaginationType & { filterId: string, randomImages?: boolean, queryId: number }
>({
Expand Down Expand Up @@ -81,17 +81,13 @@ const collectionApi = splitV3Api.injectEndpoints({
params: { includeEmpty, topLevelOnly },
}),
}),
getFilter: build.query<CollectionFilterType, { filterId?: string }>({
query: ({ filterId }) => ({ url: `Filter/${filterId}` }),
}),
}),
});

export const {
useGetFilterQuery,
useGetGroupQuery,
useLazyGetFiltersQuery,
useLazyGetGroupSeriesQuery,
useLazyGetGroupsQuery,
useLazyGetGroupsInfiniteQuery,
useLazyGetTopFiltersQuery,
} = collectionApi;
Loading

0 comments on commit 6a11b5d

Please sign in to comment.