From 6afd09a7364189b54b6ce2cf06ec13e773147f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Thu, 6 Jun 2024 11:58:58 +0200 Subject: [PATCH 01/13] refactor content search to use react query --- components/content-search/index.tsx | 330 +++++++--------------------- components/content-search/types.ts | 53 +++++ package-lock.json | 25 +++ package.json | 1 + 4 files changed, 154 insertions(+), 255 deletions(-) create mode 100644 components/content-search/types.ts diff --git a/components/content-search/index.tsx b/components/content-search/index.tsx index 27a89001..4a1effed 100644 --- a/components/content-search/index.tsx +++ b/components/content-search/index.tsx @@ -4,9 +4,24 @@ import { useState, useRef, useEffect, useCallback } from '@wordpress/element'; import { addQueryArgs } from '@wordpress/url'; import { __ } from '@wordpress/i18n'; import styled from '@emotion/styled'; +import {useMergeRefs} from '@wordpress/compose'; import SearchItem, { Suggestion } from './SearchItem'; import { StyledComponentContext } from '../styled-components-context'; +import type { QueryCache, SearchResult, ContentSearchProps } from './types'; + +import { + QueryClient, + QueryClientProvider, + useQuery, + useInfiniteQuery, + QueryFunction, + } from '@tanstack/react-query'; +import { useOnClickOutside } from '../../hooks/use-on-click-outside'; + + +const queryClient = new QueryClient(); + const NAMESPACE = 'tenup-content-search'; // Equalize height of list icons to match loader in order to reduce jumping. @@ -47,57 +62,6 @@ const StyledSearchControl = styled(SearchControl)` width: 100%; `; -interface QueryCache { - results: SearchResult[] | null; - controller: null | number | AbortController; - currentPage: number | null; - totalPages: number | null; -} - -interface SearchResult { - id: number; - title: string; - url: string; - type: string; - subtype: string; - link?: string; - name?: string; -} - -interface QueryArgs { - perPage: number; - page: number; - contentTypes: string[]; - mode: string; - keyword: string; -} - -interface RenderItemComponentProps { - item: Suggestion; - onSelect: () => void; - searchTerm?: string; - isSelected?: boolean; - id?: string; - contentTypes: string[]; - renderType?: (suggestion: Suggestion) => string; -} - -interface ContentSearchProps { - onSelectItem: (item: Suggestion) => void; - placeholder?: string; - label?: string; - hideLabelFromVision?: boolean; - contentTypes?: string[]; - mode?: 'post' | 'user' | 'term'; - perPage?: number; - queryFilter?: (query: string, args: QueryArgs) => string; - excludeItems?: { - id: number; - }[]; - renderItemType?: (props: Suggestion) => string; - renderItem?: (props: RenderItemComponentProps) => JSX.Element; - fetchInitialResults?: boolean; -} const ContentSearch: React.FC = ({ onSelectItem = () => { @@ -116,13 +80,9 @@ const ContentSearch: React.FC = ({ fetchInitialResults, }) => { const [searchString, setSearchString] = useState(''); - const [searchQueries, setSearchQueries] = useState<{[key: string]: QueryCache}>({}); const [selectedItem, setSelectedItem] = useState(null); - const [currentPage, setCurrentPage] = useState(1); const [isFocused, setIsFocused] = useState(false); - const mounted = useRef(true); - const searchContainer = useRef(null); const filterResults = useCallback( @@ -233,214 +193,65 @@ const ContentSearch: React.FC = ({ [mode, filterResults], ); - /** - * handleSearchStringChange - * - * Using the keyword and the list of tags that are linked to the parent - * block search for posts/terms/users that match and return them to the - * autocomplete component. - * - * @param {string} keyword search query string - * @param {number} page page query string - */ - const handleSearchStringChange = (keyword: string, page: number) => { - // Reset page and query on empty keyword. - if (keyword.trim() === '') { - setCurrentPage(1); - } - - const preparedQuery = prepareSearchQuery(keyword, page); - - // Only do query if not cached or previously errored/cancelled. - if (!searchQueries[preparedQuery] || searchQueries[preparedQuery].controller === 1) { - setSearchQueries((queries) => { - // New queries. - const newQueries: {[key: string]: QueryCache} = {}; - - // Remove errored or cancelled queries. - Object.keys(queries).forEach((query) => { - if (queries[query].controller !== 1) { - newQueries[query] = queries[query]; - } - }); - - newQueries[preparedQuery] = { - results: null, - controller: null, - currentPage: page, - totalPages: null, - }; - - return newQueries; - }); - } - - setCurrentPage(page); - - setSearchString(keyword); - }; - - const handleLoadMore = () => { - handleSearchStringChange(searchString, currentPage + 1); - }; - - useEffect(() => { - // Trigger initial fetch if enabled. - if (fetchInitialResults) { - handleSearchStringChange('', 1); - } - - return () => { - mounted.current = false; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - Object.keys(searchQueries).forEach((searchQueryString) => { - const searchQuery = searchQueries[searchQueryString]; - - if (searchQueryString !== prepareSearchQuery(searchString, currentPage)) { - if (searchQuery.controller && typeof searchQuery.controller === 'object') { - searchQuery.controller.abort(); - } - } else if (searchQuery.results === null && searchQuery.controller === null) { - const controller = new AbortController(); - - apiFetch({ + const clickOutsideRef = useOnClickOutside(() => { + setIsFocused(false); + }); + + const mergedRef = useMergeRefs([searchContainer, clickOutsideRef]); + + const { + status, + data, + error, + isFetching, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + } = useInfiniteQuery( + { + queryKey: ['search', searchString, contentTypes.join(','), mode, perPage], + queryFn: async ({ pageParam = 1 }) => { + const searchQueryString = prepareSearchQuery(searchString, pageParam); + const response = await apiFetch({ path: searchQueryString, - signal: controller.signal, parse: false, - }) - .then((results: Response) => { - const totalPages = parseInt( - ( results.headers && results.headers.get('X-WP-TotalPages') ) || '0', - 10, - ); - - // Parse, because we set parse to false to get the headers. - results.json().then((results: SearchResult[]) => { - if (mounted.current === false) { - return; - } - const normalizedResults = normalizeResults(results); - - setSearchQueries((queries) => { - const newQueries = { ...queries }; - - if (typeof newQueries[searchQueryString] === 'undefined') { - newQueries[searchQueryString] = { - results: null, - controller: null, - totalPages: null, - currentPage: null, - }; - } - - newQueries[searchQueryString].results = normalizedResults; - newQueries[searchQueryString].totalPages = totalPages; - newQueries[searchQueryString].controller = 0; - - return newQueries; - }); - }); - }) - .catch((error) => { - // fetch_error means the request was aborted - if (error.code !== 'fetch_error') { - setSearchQueries((queries) => { - const newQueries = { ...queries }; - - if (typeof newQueries[searchQueryString] === 'undefined') { - newQueries[searchQueryString] = { - results: null, - controller: null, - totalPages: null, - currentPage: null, - }; - } + }); - newQueries[searchQueryString].controller = 1; - newQueries[searchQueryString].results = []; + const totalPages = parseInt( + ( response.headers && response.headers.get('X-WP-TotalPages') ) || '0', + 10, + ); - return newQueries; - }); - } - }); + const results = await response.json(); + const normalizedResults = normalizeResults(results); - setSearchQueries((queries) => { - const newQueries = { ...queries }; + const hasNextPage = totalPages > pageParam; + const hasPreviousPage = pageParam > 1; - newQueries[searchQueryString].controller = controller; + return { + results: normalizedResults, + nextPage: hasNextPage ? pageParam + 1 : undefined, + previousPage: hasPreviousPage ? pageParam - 1 : undefined, + }; + }, + getNextPageParam: (lastPage) => lastPage.nextPage, + getPreviousPageParam: (firstPage) => firstPage.previousPage, + initialPageParam: 1 + } + ); - return newQueries; - }); - } - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchQueries, searchString, currentPage]); - - let searchResults: SearchResult[] | null = null; - let isLoading = true; - let showLoadMore = false; - - for (let i = 1; i <= currentPage; i++) { - // eslint-disable-next-line no-loop-func - Object.keys(searchQueries).forEach((searchQueryString) => { - const searchQuery = searchQueries[searchQueryString]; - - if (searchQueryString === prepareSearchQuery(searchString, i)) { - if (searchQuery.results !== null) { - if (searchResults === null) { - searchResults = []; - } - - searchResults = searchResults.concat(searchQuery.results); - - // If on last page, maybe show load more button - if (i === currentPage) { - isLoading = false; - - if ( searchQuery.totalPages && searchQuery.currentPage && searchQuery.totalPages > searchQuery.currentPage) { - showLoadMore = true; - } - } - } else if (searchQuery.controller === 1 && i === currentPage) { - isLoading = false; - showLoadMore = false; - } - } - }); - } + const searchResults = data?.pages.map((page) => page?.results).flat() || undefined; - if (searchResults !== null) { - searchResults = filterResults(searchResults); - } const hasSearchString = !!searchString.length; const hasSearchResults = searchResults && !!searchResults.length; const hasInitialResults = fetchInitialResults && isFocused; - // Add event listener to close search results when clicking outside of the search container. - useEffect(() => { - document.addEventListener('mouseup', (e: MouseEvent) => { - // Bail if anywhere inside search container is clicked. - if ( - searchContainer.current?.contains(e.target as Node) - ) { - return; - } - - setIsFocused(false); - }); - }, []); - return ( - - + { - handleSearchStringChange(newSearchString, 1); + setSearchString(newSearchString); }} label={label} hideLabelFromVision={hideLabelFromVision} @@ -454,9 +265,9 @@ const ContentSearch: React.FC = ({ {hasSearchString || hasInitialResults ? ( <> - {isLoading && currentPage === 1 && } + {status === 'pending' && } - {!isLoading && !hasSearchResults && ( + {!!error || (!isFetching && !hasSearchResults) && (
  • = ({
  • )} { - (!isLoading || currentPage > 1) && + status === 'success' && searchResults && searchResults.map((item, index) => { - if (!item.title.length) { + if (!item || !item.title.length) { return null; } @@ -514,20 +325,29 @@ const ContentSearch: React.FC = ({ })}
    - {!isLoading && hasSearchResults && showLoadMore && ( + {hasSearchResults && hasNextPage && ( - )} - {isLoading && currentPage > 1 && } + {isFetchingNextPage && } ) : null}
    + ); +}; + +const ContentSearchWrapper: React.FC = (props) => { + return ( + + + + ); }; -export { ContentSearch }; +export { ContentSearchWrapper as ContentSearch}; diff --git a/components/content-search/types.ts b/components/content-search/types.ts new file mode 100644 index 00000000..763c47b3 --- /dev/null +++ b/components/content-search/types.ts @@ -0,0 +1,53 @@ +import type { Suggestion } from './SearchItem'; + +export interface QueryCache { + results: SearchResult[] | null; + controller: null | number | AbortController; + currentPage: number | null; + totalPages: number | null; +} + +export interface SearchResult { + id: number; + title: string; + url: string; + type: string; + subtype: string; + link?: string; + name?: string; +} + +export interface QueryArgs { + perPage: number; + page: number; + contentTypes: string[]; + mode: string; + keyword: string; +} + +export interface RenderItemComponentProps { + item: Suggestion; + onSelect: () => void; + searchTerm?: string; + isSelected?: boolean; + id?: string; + contentTypes: string[]; + renderType?: (suggestion: Suggestion) => string; +} + +export interface ContentSearchProps { + onSelectItem: (item: Suggestion) => void; + placeholder?: string; + label?: string; + hideLabelFromVision?: boolean; + contentTypes?: string[]; + mode?: 'post' | 'user' | 'term'; + perPage?: number; + queryFilter?: (query: string, args: QueryArgs) => string; + excludeItems?: { + id: number; + }[]; + renderItemType?: (props: Suggestion) => string; + renderItem?: (props: RenderItemComponentProps) => JSX.Element; + fetchInitialResults?: boolean; +} diff --git a/package-lock.json b/package-lock.json index d8b06d11..89f568af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@emotion/styled": "^11.11.5", "@floating-ui/react-dom": "^2.0.9", "@leeoniya/ufuzzy": "^1.0.14", + "@tanstack/react-query": "^5.40.1", "@wordpress/icons": "^9.48.0", "array-move": "^4.0.0", "prop-types": "^15.8.1", @@ -5935,6 +5936,30 @@ "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==", "dev": true }, + "node_modules/@tanstack/query-core": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.40.0.tgz", + "integrity": "sha512-eD8K8jsOIq0Z5u/QbvOmfvKKE/XC39jA7yv4hgpl/1SRiU+J8QCIwgM/mEHuunQsL87dcvnHqSVLmf9pD4CiaA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.40.1.tgz", + "integrity": "sha512-gOcmu+gpFd2taHrrgMM9RemLYYEDYfsCqszxCC0xtx+csDa4R8t7Hr7SfWXQP13S2sF+mOxySo/+FNXJFYBqcA==", + "dependencies": { + "@tanstack/query-core": "5.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", diff --git a/package.json b/package.json index 2763b431..40ab3e2e 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@emotion/styled": "^11.11.5", "@floating-ui/react-dom": "^2.0.9", "@leeoniya/ufuzzy": "^1.0.14", + "@tanstack/react-query": "^5.40.1", "@wordpress/icons": "^9.48.0", "array-move": "^4.0.0", "prop-types": "^15.8.1", From c21d48f186c3ebdd9da53a0e6fa50f91bafc30cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Thu, 6 Jun 2024 12:01:56 +0200 Subject: [PATCH 02/13] remove unused variables --- components/content-search/index.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/components/content-search/index.tsx b/components/content-search/index.tsx index 4a1effed..31e446db 100644 --- a/components/content-search/index.tsx +++ b/components/content-search/index.tsx @@ -1,6 +1,6 @@ import { Spinner, NavigableMenu, Button, SearchControl } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; -import { useState, useRef, useEffect, useCallback } from '@wordpress/element'; +import { useState, useRef, useCallback } from '@wordpress/element'; import { addQueryArgs } from '@wordpress/url'; import { __ } from '@wordpress/i18n'; import styled from '@emotion/styled'; @@ -8,14 +8,12 @@ import {useMergeRefs} from '@wordpress/compose'; import SearchItem, { Suggestion } from './SearchItem'; import { StyledComponentContext } from '../styled-components-context'; -import type { QueryCache, SearchResult, ContentSearchProps } from './types'; +import type { SearchResult, ContentSearchProps } from './types'; import { QueryClient, QueryClientProvider, - useQuery, useInfiniteQuery, - QueryFunction, } from '@tanstack/react-query'; import { useOnClickOutside } from '../../hooks/use-on-click-outside'; From fc9ffe644cfc02753865bdc836d98547318c17ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Thu, 6 Jun 2024 12:02:50 +0200 Subject: [PATCH 03/13] remove no longer needed types --- components/content-search/types.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/components/content-search/types.ts b/components/content-search/types.ts index 763c47b3..d4b21710 100644 --- a/components/content-search/types.ts +++ b/components/content-search/types.ts @@ -1,12 +1,5 @@ import type { Suggestion } from './SearchItem'; -export interface QueryCache { - results: SearchResult[] | null; - controller: null | number | AbortController; - currentPage: number | null; - totalPages: number | null; -} - export interface SearchResult { id: number; title: string; From 77ede04084c41e08b1c04fa1e14b7996fee0c486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Thu, 6 Jun 2024 12:06:03 +0200 Subject: [PATCH 04/13] fix ensure all dependencies are listed in query filter --- components/content-search/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/content-search/index.tsx b/components/content-search/index.tsx index 31e446db..9ab365e2 100644 --- a/components/content-search/index.tsx +++ b/components/content-search/index.tsx @@ -207,7 +207,14 @@ const ContentSearch: React.FC = ({ hasNextPage, } = useInfiniteQuery( { - queryKey: ['search', searchString, contentTypes.join(','), mode, perPage], + queryKey: [ + 'search', + searchString, + contentTypes.join(','), + mode, + perPage, + queryFilter, + ], queryFn: async ({ pageParam = 1 }) => { const searchQueryString = prepareSearchQuery(searchString, pageParam); const response = await apiFetch({ From 2ef2087e054146b191388ea7d2d41b051185877e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Fri, 7 Jun 2024 11:11:35 +0200 Subject: [PATCH 05/13] do additional clean up work of the content search component --- components/content-search/index.tsx | 80 ++++++------------ components/content-search/types.ts | 19 +++-- components/content-search/utils.ts | 121 ++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 61 deletions(-) create mode 100644 components/content-search/utils.ts diff --git a/components/content-search/index.tsx b/components/content-search/index.tsx index 9ab365e2..e8d9e45e 100644 --- a/components/content-search/index.tsx +++ b/components/content-search/index.tsx @@ -7,8 +7,8 @@ import styled from '@emotion/styled'; import {useMergeRefs} from '@wordpress/compose'; import SearchItem, { Suggestion } from './SearchItem'; import { StyledComponentContext } from '../styled-components-context'; - -import type { SearchResult, ContentSearchProps } from './types'; +import type { ContentSearchProps } from './types'; +import type { WP_REST_API_User, WP_REST_API_Post, WP_REST_API_Term } from 'wp-types'; import { QueryClient, @@ -16,12 +16,10 @@ import { useInfiniteQuery, } from '@tanstack/react-query'; import { useOnClickOutside } from '../../hooks/use-on-click-outside'; - +import { normalizeResults } from './utils'; const queryClient = new QueryClient(); -const NAMESPACE = 'tenup-content-search'; - // Equalize height of list icons to match loader in order to reduce jumping. const listMinHeight = '46px'; @@ -60,7 +58,6 @@ const StyledSearchControl = styled(SearchControl)` width: 100%; `; - const ContentSearch: React.FC = ({ onSelectItem = () => { console.log('Select!'); // eslint-disable-line no-console @@ -83,20 +80,6 @@ const ContentSearch: React.FC = ({ const searchContainer = useRef(null); - const filterResults = useCallback( - (results: SearchResult[]) => { - return results.filter((result: SearchResult) => { - let keep = true; - - if (excludeItems.length) { - keep = excludeItems.every((item) => item.id !== result.id); - } - - return keep; - }); - }, - [excludeItems], - ); /** * handleSelection @@ -106,7 +89,7 @@ const ContentSearch: React.FC = ({ * * @param {number} item item */ - const handleOnNavigate = (item: number) => { + const handleSuggestionSelection = (item: number) => { if (item === 0) { setSelectedItem(null); } @@ -163,34 +146,6 @@ const ContentSearch: React.FC = ({ [perPage, contentTypes, mode, queryFilter], ); - /** - * Depending on the mode value, this method normalizes the format - * of the result array. - * - * @param {string} mode ContentPicker mode. - * @param {SearchResult[]} result The array to be normalized. - * - * @returns {SearchResult[]} The normalizes array. - */ - const normalizeResults = useCallback( - (result: SearchResult[] = []): SearchResult[] => { - const normalizedResults = filterResults(result); - - if (mode === 'user') { - return normalizedResults.map((item) => ({ - id: item.id, - subtype: mode, - title: item.name || '', - type: mode, - url: item.link || '', - } as SearchResult)); - } - - return normalizedResults; - }, - [mode, filterResults], - ); - const clickOutsideRef = useOnClickOutside(() => { setIsFocused(false); }); @@ -227,8 +182,21 @@ const ContentSearch: React.FC = ({ 10, ); - const results = await response.json(); - const normalizedResults = normalizeResults(results); + let results: WP_REST_API_User[] | WP_REST_API_Post[] | WP_REST_API_Term[]; + + switch (mode) { + case 'user': + results = await response.json() as WP_REST_API_User[]; + break; + case 'post': + results = await response.json() as WP_REST_API_Post[]; + break; + case 'term': + results = await response.json() as WP_REST_API_Term[]; + break; + } + + const normalizedResults = normalizeResults({results, excludeItems, mode}); const hasNextPage = totalPages > pageParam; const hasPreviousPage = pageParam > 1; @@ -252,7 +220,7 @@ const ContentSearch: React.FC = ({ const hasInitialResults = fetchInitialResults && isFocused; return ( - + { @@ -269,12 +237,12 @@ const ContentSearch: React.FC = ({ {hasSearchString || hasInitialResults ? ( <> - + {status === 'pending' && } {!!error || (!isFetching && !hasSearchResults) && (
  • = ({ status === 'success' && searchResults && searchResults.map((item, index) => { - if (!item || !item.title.length) { + if (!item || !item?.title?.length) { return null; } @@ -301,7 +269,7 @@ const ContentSearch: React.FC = ({ return (
  • ; + mode: ContentSearchMode; keyword: string; } @@ -24,17 +29,19 @@ export interface RenderItemComponentProps { searchTerm?: string; isSelected?: boolean; id?: string; - contentTypes: string[]; + contentTypes: Array; renderType?: (suggestion: Suggestion) => string; } +export type ContentSearchMode = 'post' | 'user' | 'term'; + export interface ContentSearchProps { onSelectItem: (item: Suggestion) => void; placeholder?: string; label?: string; hideLabelFromVision?: boolean; - contentTypes?: string[]; - mode?: 'post' | 'user' | 'term'; + contentTypes?: Array; + mode?: ContentSearchMode; perPage?: number; queryFilter?: (query: string, args: QueryArgs) => string; excludeItems?: { @@ -44,3 +51,5 @@ export interface ContentSearchProps { renderItem?: (props: RenderItemComponentProps) => JSX.Element; fetchInitialResults?: boolean; } + +export type Modify = Omit & R; diff --git a/components/content-search/utils.ts b/components/content-search/utils.ts new file mode 100644 index 00000000..b079b922 --- /dev/null +++ b/components/content-search/utils.ts @@ -0,0 +1,121 @@ +import type { ContentSearchMode, Modify } from "./types"; +import type { WP_REST_API_User, WP_REST_API_Post, WP_REST_API_Term } from "wp-types"; + +interface IdentifiableObject extends Object { + id: number; +}; + +interface FilterResultsArgs { + results: WP_REST_API_User[] | WP_REST_API_Post[] | WP_REST_API_Term[]; + excludeItems: Array; +} + +/** + * Filters results. + * + * @returns Filtered results. + */ +export const filterResults = ({ results, excludeItems }: FilterResultsArgs) => { + return results.filter((result) => { + let keep = true; + + if (excludeItems.length) { + keep = excludeItems.every((item) => item.id !== result.id); + } + + return keep; + }); +}; + +interface PrepareSearchQueryArgs { + keyword: string; + page: number; + mode: ContentSearchMode; + perPage: number; + contentTypes: Array; + queryFilter: (queryString: string, options: { + perPage: number; + page: number; + contentTypes: Array; + mode: ContentSearchMode; + keyword: string; + }) => string; +} + +/** + * Prepares a search query based on the given keyword and page number. + * + * @returns The prepared search query. + */ +export const prepareSearchQuery = ({ keyword, page, mode, perPage, contentTypes, queryFilter }: PrepareSearchQueryArgs): string => { + let searchQuery; + + switch (mode) { + case 'user': + searchQuery = `wp/v2/users/?search=${keyword}`; + break; + default: + searchQuery = `wp/v2/search/?search=${keyword}&subtype=${contentTypes.join( + ',', + )}&type=${mode}&_embed&per_page=${perPage}&page=${page}`; + break; + } + + return queryFilter(searchQuery, { + perPage, + page, + contentTypes, + mode, + keyword, + }); +}; + +interface NormalizeResultsArgs { + mode: ContentSearchMode; + results: WP_REST_API_Post[] | WP_REST_API_User[] | WP_REST_API_Term[] + excludeItems: Array; +} + +/** + * Depending on the mode value, this method normalizes the format + * of the result array. + * + * @returns Normalized results. + */ +export const normalizeResults = ({ mode, results, excludeItems }: NormalizeResultsArgs): Array<{ + id: number; + subtype: ContentSearchMode; + title: string; + type: ContentSearchMode; + url: string; +}> => { + const normalizedResults = filterResults({ results, excludeItems }); + + return normalizedResults.map((item) => { + + let title: string; + + switch (mode) { + case 'post': + const postItem = item as unknown as Modify; + title = postItem.title; + break; + case 'term': + const termItem = item as WP_REST_API_Term; + title = termItem.name; + break; + case 'user': + const userItem = item as WP_REST_API_User; + title = userItem.name; + break; + } + + return { + id: item.id, + subtype: mode, + title: title, + type: mode, + url: item.link, + }; + }); +}; From 27a3fb0308ac173ddcb99ecadc3d648355fb18bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Fri, 7 Jun 2024 12:52:40 +0200 Subject: [PATCH 06/13] harden types of content search --- components/content-search/SearchItem.tsx | 45 ++--- components/content-search/index.tsx | 227 +++++++---------------- components/content-search/types.ts | 34 +--- components/content-search/utils.ts | 132 ++++++++----- 4 files changed, 180 insertions(+), 258 deletions(-) diff --git a/components/content-search/SearchItem.tsx b/components/content-search/SearchItem.tsx index 4bb29a8e..453c79bd 100644 --- a/components/content-search/SearchItem.tsx +++ b/components/content-search/SearchItem.tsx @@ -8,6 +8,8 @@ import { __experimentalTruncate as Truncate, } from '@wordpress/components'; import { getTextContent, create } from '@wordpress/rich-text'; +import { RenderItemComponentProps } from './types'; +import { NormalizedSuggestion } from './utils'; const ButtonStyled = styled(Button)` display: flex; @@ -50,29 +52,10 @@ const ButtonStyled = styled(Button)` } `; -export interface Suggestion { - id: number; - title: string; - url: string; - type: string; - subtype: string; -} - -interface SearchItemProps { - suggestion: Suggestion; - onClick: () => void; - searchTerm?: string; - isSelected?: boolean; - id?: string; - contentTypes: string[]; - renderType?: (suggestion: Suggestion) => string; -} - -const SearchItem: React.FC = ({ - suggestion, - onClick, +const SearchItem: React.FC = ({ + item: suggestion, + onSelect: onClick, searchTerm = '', - isSelected = false, id = '', contentTypes, renderType = defaultRenderItemType, @@ -88,9 +71,7 @@ const SearchItem: React.FC = ({ = ({ ); }; -export function defaultRenderItemType(suggestion: Suggestion): string { - // Rename 'post_tag' to 'tag'. Ideally, the API would return the localised CPT or taxonomy label. - return suggestion.type === 'post_tag' ? 'tag' : suggestion.subtype; +export function defaultRenderItemType(suggestion: NormalizedSuggestion): string { + // Rename 'post_tag' to 'tag'. Ideally, the API would return the localized CPT or taxonomy label. + if ( suggestion.type === 'post_tag' ) { + return 'tag'; + } + + if ( suggestion.subtype ) { + return suggestion.subtype; + } + + return suggestion.type; } export default SearchItem; diff --git a/components/content-search/index.tsx b/components/content-search/index.tsx index e8d9e45e..aac437d0 100644 --- a/components/content-search/index.tsx +++ b/components/content-search/index.tsx @@ -1,22 +1,18 @@ import { Spinner, NavigableMenu, Button, SearchControl } from '@wordpress/components'; -import apiFetch from '@wordpress/api-fetch'; -import { useState, useRef, useCallback } from '@wordpress/element'; -import { addQueryArgs } from '@wordpress/url'; +import { useState, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import styled from '@emotion/styled'; import {useMergeRefs} from '@wordpress/compose'; -import SearchItem, { Suggestion } from './SearchItem'; +import SearchItem from './SearchItem'; import { StyledComponentContext } from '../styled-components-context'; -import type { ContentSearchProps } from './types'; -import type { WP_REST_API_User, WP_REST_API_Post, WP_REST_API_Term } from 'wp-types'; - +import type { ContentSearchMode, IdentifiableObject, QueryFilter, RenderItemComponentProps } from './types'; import { QueryClient, QueryClientProvider, useInfiniteQuery, } from '@tanstack/react-query'; import { useOnClickOutside } from '../../hooks/use-on-click-outside'; -import { normalizeResults } from './utils'; +import { NormalizedSuggestion, fetchSearchResults } from './utils'; const queryClient = new QueryClient(); @@ -31,6 +27,10 @@ const List = styled.ul` padding: 0 !important; `; +const ListItem = styled.li` + margin-bottom: 0; +`; + const StyledSpinner = styled(Spinner)` /* Custom styles to reduce jumping while loading the results */ min-height: ${listMinHeight}; @@ -58,6 +58,33 @@ const StyledSearchControl = styled(SearchControl)` width: 100%; `; +const StyledNoResults = styled.li` + color: inherit; + cursor: default; + padding-left: 3px; +`; + +const ContentSearchNoResults: React.FC = () => ( + + {__('Nothing found.', '10up-block-components')} + +); + +export interface ContentSearchProps { + onSelectItem: (item: NormalizedSuggestion) => void; + placeholder?: string; + label?: string; + hideLabelFromVision?: boolean; + contentTypes?: Array; + mode?: ContentSearchMode; + perPage?: number; + queryFilter?: QueryFilter; + excludeItems?: Array; + renderItemType?: (props: NormalizedSuggestion) => string; + renderItem?: (props: RenderItemComponentProps) => JSX.Element; + fetchInitialResults?: boolean; +} + const ContentSearch: React.FC = ({ onSelectItem = () => { console.log('Select!'); // eslint-disable-line no-console @@ -71,81 +98,19 @@ const ContentSearch: React.FC = ({ queryFilter = (query: string) => query, excludeItems = [], renderItemType = undefined, - renderItem: RenderItemComponent = undefined, + renderItem: SearchResultItem = SearchItem, fetchInitialResults, }) => { const [searchString, setSearchString] = useState(''); - const [selectedItem, setSelectedItem] = useState(null); const [isFocused, setIsFocused] = useState(false); - const searchContainer = useRef(null); - - /** - * handleSelection - * - * update the selected item in state to either the selected item or null if the - * selected item does not have a valid id - * - * @param {number} item item - */ - const handleSuggestionSelection = (item: number) => { - if (item === 0) { - setSelectedItem(null); - } - - setSelectedItem(item); - }; - - /** - * handleItemSelection - * - * reset the search input & item container - * trigger the onSelectItem callback passed in via props - * - * @param {Suggestion} item item - */ - const handleItemSelection = (item: Suggestion) => { + const handleItemSelection = (item: NormalizedSuggestion) => { setSearchString(''); setIsFocused(false); - onSelectItem(item); }; - const prepareSearchQuery = useCallback( - (keyword: string, page: number) => { - let searchQuery; - - switch (mode) { - case 'user': - searchQuery = addQueryArgs('wp/v2/users', { - search: keyword, - }); - break; - default: - searchQuery = addQueryArgs('wp/v2/search', { - search: keyword, - subtype: contentTypes.join(','), - type: mode, - _embed: true, - per_page: perPage, - page, - }); - - break; - } - - return queryFilter(searchQuery, { - perPage, - page, - contentTypes, - mode, - keyword, - }); - }, - [perPage, contentTypes, mode, queryFilter], - ); - const clickOutsideRef = useOnClickOutside(() => { setIsFocused(false); }); @@ -170,43 +135,15 @@ const ContentSearch: React.FC = ({ perPage, queryFilter, ], - queryFn: async ({ pageParam = 1 }) => { - const searchQueryString = prepareSearchQuery(searchString, pageParam); - const response = await apiFetch({ - path: searchQueryString, - parse: false, - }); - - const totalPages = parseInt( - ( response.headers && response.headers.get('X-WP-TotalPages') ) || '0', - 10, - ); - - let results: WP_REST_API_User[] | WP_REST_API_Post[] | WP_REST_API_Term[]; - - switch (mode) { - case 'user': - results = await response.json() as WP_REST_API_User[]; - break; - case 'post': - results = await response.json() as WP_REST_API_Post[]; - break; - case 'term': - results = await response.json() as WP_REST_API_Term[]; - break; - } - - const normalizedResults = normalizeResults({results, excludeItems, mode}); - - const hasNextPage = totalPages > pageParam; - const hasPreviousPage = pageParam > 1; - - return { - results: normalizedResults, - nextPage: hasNextPage ? pageParam + 1 : undefined, - previousPage: hasPreviousPage ? pageParam - 1 : undefined, - }; - }, + queryFn: async ({ pageParam = 1 }) => fetchSearchResults({ + keyword: searchString, + page: pageParam, + mode, + perPage, + contentTypes, + queryFilter, + excludeItems, + }), getNextPageParam: (lastPage) => lastPage.nextPage, getPreviousPageParam: (firstPage) => firstPage.previousPage, initialPageParam: 1 @@ -214,13 +151,16 @@ const ContentSearch: React.FC = ({ ); const searchResults = data?.pages.map((page) => page?.results).flat() || undefined; + console.log({searchResults}); const hasSearchString = !!searchString.length; - const hasSearchResults = searchResults && !!searchResults.length; + const hasSearchResults = status === 'success' && searchResults && !!searchResults.length; const hasInitialResults = fetchInitialResults && isFocused; + const hasNoResults = !!error || (!isFetching && !hasSearchResults); + const isPending = status === 'pending'; return ( - + { @@ -238,62 +178,23 @@ const ContentSearch: React.FC = ({ {hasSearchString || hasInitialResults ? ( <> - {status === 'pending' && } - - {!!error || (!isFetching && !hasSearchResults) && ( -
  • - {__('Nothing found.', '10up-block-components')} -
  • - )} - { - status === 'success' && - searchResults && - searchResults.map((item, index) => { - if (!item || !item?.title?.length) { - return null; - } - - const isSelected = selectedItem === index + 1; - + {isPending && } + {hasNoResults && } + {hasSearchResults && + searchResults.map((item) => { const selectItem = () => { handleItemSelection(item); }; - return ( -
  • - {RenderItemComponent ? ( - - ) : ( - - )} -
  • + + + ); })}
    diff --git a/components/content-search/types.ts b/components/content-search/types.ts index bdc5b0be..30421aca 100644 --- a/components/content-search/types.ts +++ b/components/content-search/types.ts @@ -1,9 +1,8 @@ -import type { - WP_REST_API_User, - WP_REST_API_Post, - WP_REST_API_Term, -} from 'wp-types'; -import type { Suggestion } from './SearchItem'; +import { NormalizedSuggestion } from './utils'; + +export interface IdentifiableObject { + id: number; +} export interface SearchResult { id: number; @@ -23,33 +22,18 @@ export interface QueryArgs { keyword: string; } +export type QueryFilter = (query: string, args: QueryArgs) => string; + export interface RenderItemComponentProps { - item: Suggestion; + item: NormalizedSuggestion; onSelect: () => void; searchTerm?: string; isSelected?: boolean; id?: string; contentTypes: Array; - renderType?: (suggestion: Suggestion) => string; + renderType?: (suggestion: NormalizedSuggestion) => string; } export type ContentSearchMode = 'post' | 'user' | 'term'; -export interface ContentSearchProps { - onSelectItem: (item: Suggestion) => void; - placeholder?: string; - label?: string; - hideLabelFromVision?: boolean; - contentTypes?: Array; - mode?: ContentSearchMode; - perPage?: number; - queryFilter?: (query: string, args: QueryArgs) => string; - excludeItems?: { - id: number; - }[]; - renderItemType?: (props: Suggestion) => string; - renderItem?: (props: RenderItemComponentProps) => JSX.Element; - fetchInitialResults?: boolean; -} - export type Modify = Omit & R; diff --git a/components/content-search/utils.ts b/components/content-search/utils.ts index b079b922..c0083f54 100644 --- a/components/content-search/utils.ts +++ b/components/content-search/utils.ts @@ -1,21 +1,18 @@ -import type { ContentSearchMode, Modify } from "./types"; -import type { WP_REST_API_User, WP_REST_API_Post, WP_REST_API_Term } from "wp-types"; +import type { ContentSearchMode, Modify, QueryFilter } from "./types"; +import type { WP_REST_API_User, WP_REST_API_Search_Result } from "wp-types"; +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; interface IdentifiableObject extends Object { id: number; }; interface FilterResultsArgs { - results: WP_REST_API_User[] | WP_REST_API_Post[] | WP_REST_API_Term[]; + results: WP_REST_API_User[] | WP_REST_API_Search_Result[]; excludeItems: Array; } -/** - * Filters results. - * - * @returns Filtered results. - */ -export const filterResults = ({ results, excludeItems }: FilterResultsArgs) => { +export const filterOutExcludedItems = ({ results, excludeItems }: FilterResultsArgs) => { return results.filter((result) => { let keep = true; @@ -33,13 +30,7 @@ interface PrepareSearchQueryArgs { mode: ContentSearchMode; perPage: number; contentTypes: Array; - queryFilter: (queryString: string, options: { - perPage: number; - page: number; - contentTypes: Array; - mode: ContentSearchMode; - keyword: string; - }) => string; + queryFilter: QueryFilter } /** @@ -52,12 +43,20 @@ export const prepareSearchQuery = ({ keyword, page, mode, perPage, contentTypes, switch (mode) { case 'user': - searchQuery = `wp/v2/users/?search=${keyword}`; + searchQuery = addQueryArgs('wp/v2/users', { + search: keyword, + }); break; default: - searchQuery = `wp/v2/search/?search=${keyword}&subtype=${contentTypes.join( - ',', - )}&type=${mode}&_embed&per_page=${perPage}&page=${page}`; + searchQuery = addQueryArgs('wp/v2/search', { + search: keyword, + subtype: contentTypes.join(','), + type: mode, + _embed: true, + per_page: perPage, + page, + }); + break; } @@ -72,50 +71,99 @@ export const prepareSearchQuery = ({ keyword, page, mode, perPage, contentTypes, interface NormalizeResultsArgs { mode: ContentSearchMode; - results: WP_REST_API_Post[] | WP_REST_API_User[] | WP_REST_API_Term[] + results: WP_REST_API_Search_Result[] | WP_REST_API_User[] excludeItems: Array; } /** * Depending on the mode value, this method normalizes the format * of the result array. - * - * @returns Normalized results. */ export const normalizeResults = ({ mode, results, excludeItems }: NormalizeResultsArgs): Array<{ id: number; - subtype: ContentSearchMode; + subtype: ContentSearchMode | string; title: string; - type: ContentSearchMode; + type: ContentSearchMode | string; url: string; }> => { - const normalizedResults = filterResults({ results, excludeItems }); - - return normalizedResults.map((item) => { - + const filteredResults = filterOutExcludedItems({ results, excludeItems }); + return filteredResults.map((item) => { let title: string; + let url: string; + let type: string; + let subtype: string; switch (mode) { - case 'post': - const postItem = item as unknown as Modify; - title = postItem.title; - break; - case 'term': - const termItem = item as WP_REST_API_Term; - title = termItem.name; - break; case 'user': const userItem = item as WP_REST_API_User; title = userItem.name; + url = userItem.link; + type = mode; + subtype = mode; + break; + default: + const searchItem = item as WP_REST_API_Search_Result; + title = searchItem.title; + url = searchItem.url; + type = searchItem.type; + subtype = searchItem.subtype; break; } return { - id: item.id, - subtype: mode, - title: title, - type: mode, - url: item.link, + id: item.id as number, + subtype, + title, + type, + url, }; }); }; + +export type NormalizedSuggestions = ReturnType; +export type NormalizedSuggestion = NormalizedSuggestions[number]; + +interface FetchSearchResultsArgs { + keyword: string; + page: number; + mode: ContentSearchMode; + perPage: number; + contentTypes: Array; + queryFilter: QueryFilter; + excludeItems: Array; +} + +export async function fetchSearchResults({ keyword, page, mode, perPage, contentTypes, queryFilter, excludeItems }: FetchSearchResultsArgs) { + const searchQueryString = prepareSearchQuery({keyword, page, mode, perPage, contentTypes, queryFilter}); + const response = await apiFetch({ + path: searchQueryString, + parse: false, + }); + + const totalPages = parseInt( + ( response.headers && response.headers.get('X-WP-TotalPages') ) || '0', + 10, + ); + + let results: WP_REST_API_User[] | WP_REST_API_Search_Result[]; + + switch (mode) { + case 'user': + results = await response.json() as WP_REST_API_User[]; + break; + default: + results = await response.json() as WP_REST_API_Search_Result[]; + break; + } + + const normalizedResults = normalizeResults({results, excludeItems, mode}); + + const hasNextPage = totalPages > page; + const hasPreviousPage = page > 1; + + return { + results: normalizedResults, + nextPage: hasNextPage ? page + 1 : undefined, + previousPage: hasPreviousPage ? page - 1 : undefined, + }; +} From ddc9a2abf2246aaad80c22e418e3b8f210d04f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Fri, 7 Jun 2024 13:19:41 +0200 Subject: [PATCH 07/13] fix clean up SearchItem component --- components/content-search/SearchItem.tsx | 91 ++++++++++-------------- 1 file changed, 39 insertions(+), 52 deletions(-) diff --git a/components/content-search/SearchItem.tsx b/components/content-search/SearchItem.tsx index 453c79bd..e1ec2d34 100644 --- a/components/content-search/SearchItem.tsx +++ b/components/content-search/SearchItem.tsx @@ -11,13 +11,14 @@ import { getTextContent, create } from '@wordpress/rich-text'; import { RenderItemComponentProps } from './types'; import { NormalizedSuggestion } from './utils'; -const ButtonStyled = styled(Button)` +const SearchItemWrapper = styled(Button)` display: flex; text-align: left; width: 100%; justify-content: space-between; align-items: center; border-radius: 2px; + box-sizing: border-box; height: auto !important; padding: 0.3em 0.7em; overflow: hidden; @@ -26,30 +27,35 @@ const ButtonStyled = styled(Button)` /* Add opacity background to support future color changes */ /* Reduce background from #ddd to 0.05 for text contrast */ background-color: rgba(0, 0, 0, 0.05); - - .block-editor-link-control__search-item-type { - color: black; - } } +`; - .block-editor-link-control__search-item-type { - background-color: rgba(0, 0, 0, 0.05); - padding: 2px 4px; - text-transform: capitalize; - border-radius: 2px; - flex-shrink: 0; - } +const SearchItemHeader = styled.span` + display: flex; + flex-direction: column; + align-items: flex-start; +`; - .block-editor-link-control__search-item-header { - display: flex; - flex-direction: column; - align-items: flex-start; - } +const SearchItemTitle = styled.span<{showType: boolean}>` + padding-right: ${({ showType }) => (showType ? 0 : undefined)}; +`; - mark { - padding: 0 !important; - margin: 0 !important; - } +const SearchItemInfo = styled.span<{showType: boolean}>` + padding-right: ${({ showType }) => (showType ? 0 : undefined)}; +`; + +const SearchItemType = styled.span` + background-color: rgba(0, 0, 0, 0.05); + color: black; + padding: 2px 4px; + text-transform: capitalize; + border-radius: 2px; + flex-shrink: 0; +`; + +const StyledTextHighlight = styled(TextHighlight)` + margin: 0 !important; + padding: 0 !important; `; const SearchItem: React.FC = ({ @@ -60,7 +66,7 @@ const SearchItem: React.FC = ({ contentTypes, renderType = defaultRenderItemType, }) => { - const showType = suggestion.type && contentTypes.length > 1; + const showType = !!(suggestion.type && contentTypes.length > 1); const richTextContent = create({ html: suggestion.title }); const textContent = getTextContent(richTextContent); @@ -68,42 +74,23 @@ const SearchItem: React.FC = ({ return ( - - - - - - + + + + + + {filterURLForDisplay(safeDecodeURI(suggestion.url)) || ''} - - + + {showType && ( - + {renderType(suggestion)} - + )} - + ); }; From cd03b9d7b22665bc9fb194f75922c1399d72e878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Fri, 7 Jun 2024 13:24:15 +0200 Subject: [PATCH 08/13] simplify code --- components/content-search/utils.ts | 39 ++++++++++++------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/components/content-search/utils.ts b/components/content-search/utils.ts index c0083f54..8384174b 100644 --- a/components/content-search/utils.ts +++ b/components/content-search/utils.ts @@ -88,35 +88,26 @@ export const normalizeResults = ({ mode, results, excludeItems }: NormalizeResul }> => { const filteredResults = filterOutExcludedItems({ results, excludeItems }); return filteredResults.map((item) => { - let title: string; - let url: string; - let type: string; - let subtype: string; - switch (mode) { case 'user': const userItem = item as WP_REST_API_User; - title = userItem.name; - url = userItem.link; - type = mode; - subtype = mode; - break; + return { + id: userItem.id, + subtype: mode, + title: userItem.name, + type: mode, + url: userItem.link, + }; default: const searchItem = item as WP_REST_API_Search_Result; - title = searchItem.title; - url = searchItem.url; - type = searchItem.type; - subtype = searchItem.subtype; - break; - } - - return { - id: item.id as number, - subtype, - title, - type, - url, - }; + return { + id: searchItem.id as number, + subtype: searchItem.subtype, + title: searchItem.title, + type: searchItem.type, + url: searchItem.url, + }; + } }); }; From 3c8659c803a5de3f777219e14bf25de78104254e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Fri, 7 Jun 2024 15:50:42 +0200 Subject: [PATCH 09/13] fix cr feedback --- components/content-search/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/content-search/index.tsx b/components/content-search/index.tsx index aac437d0..1ab92c50 100644 --- a/components/content-search/index.tsx +++ b/components/content-search/index.tsx @@ -2,7 +2,7 @@ import { Spinner, NavigableMenu, Button, SearchControl } from '@wordpress/compon import { useState, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import styled from '@emotion/styled'; -import {useMergeRefs} from '@wordpress/compose'; +import { useMergeRefs } from '@wordpress/compose'; import SearchItem from './SearchItem'; import { StyledComponentContext } from '../styled-components-context'; import type { ContentSearchMode, IdentifiableObject, QueryFilter, RenderItemComponentProps } from './types'; @@ -151,7 +151,6 @@ const ContentSearch: React.FC = ({ ); const searchResults = data?.pages.map((page) => page?.results).flat() || undefined; - console.log({searchResults}); const hasSearchString = !!searchString.length; const hasSearchResults = status === 'success' && searchResults && !!searchResults.length; From cd59191bdd0a88a616eddf03cb90546d45eefbf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Mon, 10 Jun 2024 18:01:40 +0200 Subject: [PATCH 10/13] fix linting --- components/content-search/utils.ts | 78 ++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/components/content-search/utils.ts b/components/content-search/utils.ts index 8384174b..9983f1fb 100644 --- a/components/content-search/utils.ts +++ b/components/content-search/utils.ts @@ -1,15 +1,15 @@ -import type { ContentSearchMode, Modify, QueryFilter } from "./types"; -import type { WP_REST_API_User, WP_REST_API_Search_Result } from "wp-types"; +import type { WP_REST_API_User, WP_REST_API_Search_Result } from 'wp-types'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; +import type { ContentSearchMode, Modify, QueryFilter } from './types'; interface IdentifiableObject extends Object { id: number; -}; +} interface FilterResultsArgs { - results: WP_REST_API_User[] | WP_REST_API_Search_Result[]; - excludeItems: Array; + results: WP_REST_API_User[] | WP_REST_API_Search_Result[]; + excludeItems: Array; } export const filterOutExcludedItems = ({ results, excludeItems }: FilterResultsArgs) => { @@ -25,12 +25,12 @@ export const filterOutExcludedItems = ({ results, excludeItems }: FilterResultsA }; interface PrepareSearchQueryArgs { - keyword: string; - page: number; - mode: ContentSearchMode; - perPage: number; - contentTypes: Array; - queryFilter: QueryFilter + keyword: string; + page: number; + mode: ContentSearchMode; + perPage: number; + contentTypes: Array; + queryFilter: QueryFilter; } /** @@ -38,7 +38,14 @@ interface PrepareSearchQueryArgs { * * @returns The prepared search query. */ -export const prepareSearchQuery = ({ keyword, page, mode, perPage, contentTypes, queryFilter }: PrepareSearchQueryArgs): string => { +export const prepareSearchQuery = ({ + keyword, + page, + mode, + perPage, + contentTypes, + queryFilter, +}: PrepareSearchQueryArgs): string => { let searchQuery; switch (mode) { @@ -70,16 +77,20 @@ export const prepareSearchQuery = ({ keyword, page, mode, perPage, contentTypes, }; interface NormalizeResultsArgs { - mode: ContentSearchMode; - results: WP_REST_API_Search_Result[] | WP_REST_API_User[] - excludeItems: Array; + mode: ContentSearchMode; + results: WP_REST_API_Search_Result[] | WP_REST_API_User[]; + excludeItems: Array; } /** * Depending on the mode value, this method normalizes the format * of the result array. */ -export const normalizeResults = ({ mode, results, excludeItems }: NormalizeResultsArgs): Array<{ +export const normalizeResults = ({ + mode, + results, + excludeItems, +}: NormalizeResultsArgs): Array<{ id: number; subtype: ContentSearchMode | string; title: string; @@ -107,7 +118,7 @@ export const normalizeResults = ({ mode, results, excludeItems }: NormalizeResul type: searchItem.type, url: searchItem.url, }; - } + } }); }; @@ -124,15 +135,30 @@ interface FetchSearchResultsArgs { excludeItems: Array; } -export async function fetchSearchResults({ keyword, page, mode, perPage, contentTypes, queryFilter, excludeItems }: FetchSearchResultsArgs) { - const searchQueryString = prepareSearchQuery({keyword, page, mode, perPage, contentTypes, queryFilter}); +export async function fetchSearchResults({ + keyword, + page, + mode, + perPage, + contentTypes, + queryFilter, + excludeItems, +}: FetchSearchResultsArgs) { + const searchQueryString = prepareSearchQuery({ + keyword, + page, + mode, + perPage, + contentTypes, + queryFilter, + }); const response = await apiFetch({ path: searchQueryString, parse: false, }); const totalPages = parseInt( - ( response.headers && response.headers.get('X-WP-TotalPages') ) || '0', + (response.headers && response.headers.get('X-WP-TotalPages')) || '0', 10, ); @@ -140,14 +166,14 @@ export async function fetchSearchResults({ keyword, page, mode, perPage, content switch (mode) { case 'user': - results = await response.json() as WP_REST_API_User[]; - break; - default: - results = await response.json() as WP_REST_API_Search_Result[]; - break; + results = (await response.json()) as WP_REST_API_User[]; + break; + default: + results = (await response.json()) as WP_REST_API_Search_Result[]; + break; } - const normalizedResults = normalizeResults({results, excludeItems, mode}); + const normalizedResults = normalizeResults({ results, excludeItems, mode }); const hasNextPage = totalPages > page; const hasPreviousPage = page > 1; From 5f004088639cc7139e1ec88b08065ddbc26a386e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Mon, 10 Jun 2024 19:11:26 +0200 Subject: [PATCH 11/13] fix lint --- components/content-search/utils.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/components/content-search/utils.ts b/components/content-search/utils.ts index 9983f1fb..e2d6b125 100644 --- a/components/content-search/utils.ts +++ b/components/content-search/utils.ts @@ -1,7 +1,8 @@ +/* eslint-disable no-case-declarations */ import type { WP_REST_API_User, WP_REST_API_Search_Result } from 'wp-types'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; -import type { ContentSearchMode, Modify, QueryFilter } from './types'; +import type { ContentSearchMode, QueryFilter } from './types'; interface IdentifiableObject extends Object { id: number; @@ -33,10 +34,8 @@ interface PrepareSearchQueryArgs { queryFilter: QueryFilter; } -/** +/* * Prepares a search query based on the given keyword and page number. - * - * @returns The prepared search query. */ export const prepareSearchQuery = ({ keyword, @@ -82,7 +81,7 @@ interface NormalizeResultsArgs { excludeItems: Array; } -/** +/* * Depending on the mode value, this method normalizes the format * of the result array. */ From bb9ee2b678b3db919b9a6bbaad3a512e29f551c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Mon, 10 Jun 2024 19:12:35 +0200 Subject: [PATCH 12/13] fix lint --- components/content-search/SearchItem.tsx | 10 +-- components/content-search/index.tsx | 108 +++++++++++------------ 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/components/content-search/SearchItem.tsx b/components/content-search/SearchItem.tsx index e1ec2d34..bd9ea1ac 100644 --- a/components/content-search/SearchItem.tsx +++ b/components/content-search/SearchItem.tsx @@ -36,11 +36,11 @@ const SearchItemHeader = styled.span` align-items: flex-start; `; -const SearchItemTitle = styled.span<{showType: boolean}>` +const SearchItemTitle = styled.span<{ showType: boolean }>` padding-right: ${({ showType }) => (showType ? 0 : undefined)}; `; -const SearchItemInfo = styled.span<{showType: boolean}>` +const SearchItemInfo = styled.span<{ showType: boolean }>` padding-right: ${({ showType }) => (showType ? 0 : undefined)}; `; @@ -97,15 +97,15 @@ const SearchItem: React.FC = ({ export function defaultRenderItemType(suggestion: NormalizedSuggestion): string { // Rename 'post_tag' to 'tag'. Ideally, the API would return the localized CPT or taxonomy label. - if ( suggestion.type === 'post_tag' ) { + if (suggestion.type === 'post_tag') { return 'tag'; } - if ( suggestion.subtype ) { + if (suggestion.subtype) { return suggestion.subtype; } - return suggestion.type; + return suggestion.type; } export default SearchItem; diff --git a/components/content-search/index.tsx b/components/content-search/index.tsx index 1ab92c50..cf5d1771 100644 --- a/components/content-search/index.tsx +++ b/components/content-search/index.tsx @@ -10,7 +10,7 @@ import { QueryClient, QueryClientProvider, useInfiniteQuery, - } from '@tanstack/react-query'; +} from '@tanstack/react-query'; import { useOnClickOutside } from '../../hooks/use-on-click-outside'; import { NormalizedSuggestion, fetchSearchResults } from './utils'; @@ -125,7 +125,7 @@ const ContentSearch: React.FC = ({ isFetchingNextPage, fetchNextPage, hasNextPage, - } = useInfiniteQuery( + } = useInfiniteQuery( { queryKey: [ 'search', @@ -159,57 +159,57 @@ const ContentSearch: React.FC = ({ const isPending = status === 'pending'; return ( - - { - setSearchString(newSearchString); - }} - label={label} - hideLabelFromVision={hideLabelFromVision} - placeholder={placeholder} - autoComplete="off" - onFocus={() => { - setIsFocused(true); - }} - /> - - {hasSearchString || hasInitialResults ? ( - <> - - {isPending && } - {hasNoResults && } - {hasSearchResults && - searchResults.map((item) => { - const selectItem = () => { - handleItemSelection(item); - }; - return ( - - - - ); - })} - - - {hasSearchResults && hasNextPage && ( - - - - )} - - {isFetchingNextPage && } - - ) : null} - + + { + setSearchString(newSearchString); + }} + label={label} + hideLabelFromVision={hideLabelFromVision} + placeholder={placeholder} + autoComplete="off" + onFocus={() => { + setIsFocused(true); + }} + /> + + {hasSearchString || hasInitialResults ? ( + <> + + {isPending && } + {hasNoResults && } + {hasSearchResults && + searchResults.map((item) => { + const selectItem = () => { + handleItemSelection(item); + }; + return ( + + + + ); + })} + + + {hasSearchResults && hasNextPage && ( + + + + )} + + {isFetchingNextPage && } + + ) : null} + ); }; @@ -223,4 +223,4 @@ const ContentSearchWrapper: React.FC = (props) => { ); }; -export { ContentSearchWrapper as ContentSearch}; +export { ContentSearchWrapper as ContentSearch }; From 3a95ba68840f28609758761254b703959df7f7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= Date: Tue, 11 Jun 2024 08:47:43 +0200 Subject: [PATCH 13/13] fix linting --- components/content-search/SearchItem.tsx | 32 +++++------ components/content-search/index.tsx | 71 ++++++++++-------------- 2 files changed, 44 insertions(+), 59 deletions(-) diff --git a/components/content-search/SearchItem.tsx b/components/content-search/SearchItem.tsx index bd9ea1ac..f31c41ba 100644 --- a/components/content-search/SearchItem.tsx +++ b/components/content-search/SearchItem.tsx @@ -58,6 +58,19 @@ const StyledTextHighlight = styled(TextHighlight)` padding: 0 !important; `; +export function defaultRenderItemType(suggestion: NormalizedSuggestion): string { + // Rename 'post_tag' to 'tag'. Ideally, the API would return the localized CPT or taxonomy label. + if (suggestion.type === 'post_tag') { + return 'tag'; + } + + if (suggestion.subtype) { + return suggestion.subtype; + } + + return suggestion.type; +} + const SearchItem: React.FC = ({ item: suggestion, onSelect: onClick, @@ -85,27 +98,10 @@ const SearchItem: React.FC = ({ - {showType && ( - - {renderType(suggestion)} - - )} + {showType && {renderType(suggestion)}} ); }; -export function defaultRenderItemType(suggestion: NormalizedSuggestion): string { - // Rename 'post_tag' to 'tag'. Ideally, the API would return the localized CPT or taxonomy label. - if (suggestion.type === 'post_tag') { - return 'tag'; - } - - if (suggestion.subtype) { - return suggestion.subtype; - } - - return suggestion.type; -} - export default SearchItem; diff --git a/components/content-search/index.tsx b/components/content-search/index.tsx index cf5d1771..f084147c 100644 --- a/components/content-search/index.tsx +++ b/components/content-search/index.tsx @@ -3,14 +3,15 @@ import { useState, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import styled from '@emotion/styled'; import { useMergeRefs } from '@wordpress/compose'; +import { QueryClient, QueryClientProvider, useInfiniteQuery } from '@tanstack/react-query'; import SearchItem from './SearchItem'; import { StyledComponentContext } from '../styled-components-context'; -import type { ContentSearchMode, IdentifiableObject, QueryFilter, RenderItemComponentProps } from './types'; -import { - QueryClient, - QueryClientProvider, - useInfiniteQuery, -} from '@tanstack/react-query'; +import type { + ContentSearchMode, + IdentifiableObject, + QueryFilter, + RenderItemComponentProps, +} from './types'; import { useOnClickOutside } from '../../hooks/use-on-click-outside'; import { NormalizedSuggestion, fetchSearchResults } from './utils'; @@ -117,38 +118,23 @@ const ContentSearch: React.FC = ({ const mergedRef = useMergeRefs([searchContainer, clickOutsideRef]); - const { - status, - data, - error, - isFetching, - isFetchingNextPage, - fetchNextPage, - hasNextPage, - } = useInfiniteQuery( - { - queryKey: [ - 'search', - searchString, - contentTypes.join(','), - mode, - perPage, - queryFilter, - ], - queryFn: async ({ pageParam = 1 }) => fetchSearchResults({ - keyword: searchString, - page: pageParam, - mode, - perPage, - contentTypes, - queryFilter, - excludeItems, - }), + const { status, data, error, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage } = + useInfiniteQuery({ + queryKey: ['search', searchString, contentTypes.join(','), mode, perPage, queryFilter], + queryFn: async ({ pageParam = 1 }) => + fetchSearchResults({ + keyword: searchString, + page: pageParam, + mode, + perPage, + contentTypes, + queryFilter, + excludeItems, + }), getNextPageParam: (lastPage) => lastPage.nextPage, getPreviousPageParam: (firstPage) => firstPage.previousPage, - initialPageParam: 1 - } - ); + initialPageParam: 1, + }); const searchResults = data?.pages.map((page) => page?.results).flat() || undefined; @@ -162,7 +148,7 @@ const ContentSearch: React.FC = ({ { + onChange={(newSearchString: string) => { setSearchString(newSearchString); }} label={label} @@ -176,8 +162,8 @@ const ContentSearch: React.FC = ({ {hasSearchString || hasInitialResults ? ( <> - - {isPending && } + + {isPending && } {hasNoResults && } {hasSearchResults && searchResults.map((item) => { @@ -185,7 +171,10 @@ const ContentSearch: React.FC = ({ handleItemSelection(item); }; return ( - + = ({ )} - {isFetchingNextPage && } + {isFetchingNextPage && } ) : null}