From 6afd09a7364189b54b6ce2cf06ec13e773147f42 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= <mail@fabian-kaegy.de>
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<ContentSearchProps> = ({
 	onSelectItem = () => {
@@ -116,13 +80,9 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 	fetchInitialResults,
 }) => {
 	const [searchString, setSearchString] = useState('');
-	const [searchQueries, setSearchQueries] = useState<{[key: string]: QueryCache}>({});
 	const [selectedItem, setSelectedItem] = useState<number|null>(null);
-	const [currentPage, setCurrentPage] = useState(1);
 	const [isFocused, setIsFocused] = useState(false);
 
-	const mounted = useRef(true);
-
 	const searchContainer = useRef<HTMLDivElement>(null);
 
 	const filterResults = useCallback(
@@ -233,214 +193,65 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 		[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<Response>({
+	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<Response>({
 					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 (
-		<StyledComponentContext cacheKey="tenup-component-content-search">
-			<StyledNavigableMenu ref={searchContainer} onNavigate={handleOnNavigate} orientation="vertical">
+			<StyledNavigableMenu ref={mergedRef} onNavigate={handleOnNavigate} orientation="vertical">
 				<StyledSearchControl
 					value={searchString}
 					onChange={(newSearchString) => {
-						handleSearchStringChange(newSearchString, 1);
+						setSearchString(newSearchString);
 					}}
 					label={label}
 					hideLabelFromVision={hideLabelFromVision}
@@ -454,9 +265,9 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 				{hasSearchString || hasInitialResults ? (
 					<>
 						<List className={`${NAMESPACE}-list`}>
-							{isLoading && currentPage === 1 && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
+							{status === 'pending' && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
 
-							{!isLoading && !hasSearchResults && (
+							{!!error || (!isFetching && !hasSearchResults) && (
 								<li
 									className={`${NAMESPACE}-list-item components-button`}
 									style={{
@@ -469,10 +280,10 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 								</li>
 							)}
 							{
-								(!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<ContentSearchProps> = ({
 								})}
 						</List>
 
-						{!isLoading && hasSearchResults && showLoadMore && (
+						{hasSearchResults && hasNextPage && (
 							<LoadingContainer>
-								<Button onClick={handleLoadMore} variant="secondary">
+								<Button onClick={() => fetchNextPage()} variant="secondary">
 									{__('Load more', '10up-block-components')}
 								</Button>
 							</LoadingContainer>
 						)}
 
-						{isLoading && currentPage > 1 && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
+						{isFetchingNextPage && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
 					</>
 				) : null}
 			</StyledNavigableMenu>
+	);
+};
+
+const ContentSearchWrapper: React.FC<ContentSearchProps> = (props) => {
+	return (
+		<StyledComponentContext cacheKey="tenup-component-content-search">
+			<QueryClientProvider client={queryClient}>
+				<ContentSearch {...props} />
+			</QueryClientProvider>
 		</StyledComponentContext>
 	);
 };
 
-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?= <mail@fabian-kaegy.de>
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?= <mail@fabian-kaegy.de>
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?= <mail@fabian-kaegy.de>
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<ContentSearchProps> = ({
 		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<Response>({

From 2ef2087e054146b191388ea7d2d41b051185877e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= <mail@fabian-kaegy.de>
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<ContentSearchProps> = ({
 	onSelectItem = () => {
 		console.log('Select!'); // eslint-disable-line no-console
@@ -83,20 +80,6 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 
 	const searchContainer = useRef<HTMLDivElement>(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<ContentSearchProps> = ({
 	 *
 	 * @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<ContentSearchProps> = ({
 		[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<ContentSearchProps> = ({
 					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<ContentSearchProps> = ({
 	const hasInitialResults = fetchInitialResults && isFocused;
 
 	return (
-			<StyledNavigableMenu ref={mergedRef} onNavigate={handleOnNavigate} orientation="vertical">
+			<StyledNavigableMenu ref={mergedRef} onNavigate={handleSuggestionSelection} orientation="vertical">
 				<StyledSearchControl
 					value={searchString}
 					onChange={(newSearchString) => {
@@ -269,12 +237,12 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 
 				{hasSearchString || hasInitialResults ? (
 					<>
-						<List className={`${NAMESPACE}-list`}>
+						<List className={`tenup-content-search-list`}>
 							{status === 'pending' && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
 
 							{!!error || (!isFetching && !hasSearchResults) && (
 								<li
-									className={`${NAMESPACE}-list-item components-button`}
+									className={`tenup-content-search-list-item components-button`}
 									style={{
 										color: 'inherit',
 										cursor: 'default',
@@ -288,7 +256,7 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 								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<ContentSearchProps> = ({
 									return (
 										<li
 											key={item.id}
-											className={`${NAMESPACE}-list-item`}
+											className={`tenup-content-search-list-item`}
 											style={{
 												marginBottom: '0',
 											}}
diff --git a/components/content-search/types.ts b/components/content-search/types.ts
index d4b21710..bdc5b0be 100644
--- a/components/content-search/types.ts
+++ b/components/content-search/types.ts
@@ -1,3 +1,8 @@
+import type {
+	WP_REST_API_User,
+	WP_REST_API_Post,
+	WP_REST_API_Term,
+} from 'wp-types';
 import type { Suggestion } from './SearchItem';
 
 export interface SearchResult {
@@ -13,8 +18,8 @@ export interface SearchResult {
 export interface QueryArgs {
 	perPage: number;
 	page: number;
-	contentTypes: string[];
-	mode: string;
+	contentTypes: Array<string>;
+	mode: ContentSearchMode;
 	keyword: string;
 }
 
@@ -24,17 +29,19 @@ export interface RenderItemComponentProps {
 	searchTerm?: string;
 	isSelected?: boolean;
 	id?: string;
-	contentTypes: string[];
+	contentTypes: Array<string>;
 	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<string>;
+	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<T, R> = Omit<T, keyof R> & 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<IdentifiableObject>;
+}
+
+/**
+ * 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<string>;
+  queryFilter: (queryString: string, options: {
+	perPage: number;
+	page: number;
+	contentTypes: Array<string>;
+	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<IdentifiableObject>;
+}
+
+/**
+ * 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<WP_REST_API_Post, { title: string }>;
+				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?= <mail@fabian-kaegy.de>
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<SearchItemProps> = ({
-	suggestion,
-	onClick,
+const SearchItem: React.FC<RenderItemComponentProps> = ({
+	item: suggestion,
+	onSelect: onClick,
 	searchTerm = '',
-	isSelected = false,
 	id = '',
 	contentTypes,
 	renderType = defaultRenderItemType,
@@ -88,9 +71,7 @@ const SearchItem: React.FC<SearchItemProps> = ({
 			<ButtonStyled
 				id={id}
 				onClick={onClick}
-				className={`block-editor-link-control__search-item is-entity ${
-					isSelected && 'is-selected'
-				}`}
+				className={`block-editor-link-control__search-item is-entity`}
 				style={{
 					borderRadius: '0',
 					boxSizing: 'border-box',
@@ -127,9 +108,17 @@ const SearchItem: React.FC<SearchItemProps> = ({
 	);
 };
 
-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 = () => (
+	<StyledNoResults className="tenup-content-search-list-item components-button">
+		{__('Nothing found.', '10up-block-components')}
+	</StyledNoResults>
+);
+
+export interface ContentSearchProps {
+	onSelectItem: (item: NormalizedSuggestion) => void;
+	placeholder?: string;
+	label?: string;
+	hideLabelFromVision?: boolean;
+	contentTypes?: Array<string>;
+	mode?: ContentSearchMode;
+	perPage?: number;
+	queryFilter?: QueryFilter;
+	excludeItems?: Array<IdentifiableObject>;
+	renderItemType?: (props: NormalizedSuggestion) => string;
+	renderItem?: (props: RenderItemComponentProps) => JSX.Element;
+	fetchInitialResults?: boolean;
+}
+
 const ContentSearch: React.FC<ContentSearchProps> = ({
 	onSelectItem = () => {
 		console.log('Select!'); // eslint-disable-line no-console
@@ -71,81 +98,19 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 	queryFilter = (query: string) => query,
 	excludeItems = [],
 	renderItemType = undefined,
-	renderItem: RenderItemComponent = undefined,
+	renderItem: SearchResultItem = SearchItem,
 	fetchInitialResults,
 }) => {
 	const [searchString, setSearchString] = useState('');
-	const [selectedItem, setSelectedItem] = useState<number|null>(null);
 	const [isFocused, setIsFocused] = useState(false);
-
 	const searchContainer = useRef<HTMLDivElement>(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<ContentSearchProps> = ({
 				perPage,
 				queryFilter,
 			],
-			queryFn: async ({ pageParam = 1 }) => {
-				const searchQueryString = prepareSearchQuery(searchString, pageParam);
-				const response = await apiFetch<Response>({
-					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<ContentSearchProps> = ({
 	);
 
 	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 (
-			<StyledNavigableMenu ref={mergedRef} onNavigate={handleSuggestionSelection} orientation="vertical">
+			<StyledNavigableMenu ref={mergedRef} orientation="vertical">
 				<StyledSearchControl
 					value={searchString}
 					onChange={(newSearchString) => {
@@ -238,62 +178,23 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 				{hasSearchString || hasInitialResults ? (
 					<>
 						<List className={`tenup-content-search-list`}>
-							{status === 'pending' && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
-
-							{!!error || (!isFetching && !hasSearchResults) && (
-								<li
-									className={`tenup-content-search-list-item components-button`}
-									style={{
-										color: 'inherit',
-										cursor: 'default',
-										paddingLeft: '3px',
-									}}
-								>
-									{__('Nothing found.', '10up-block-components')}
-								</li>
-							)}
-							{
-								status === 'success' &&
-								searchResults &&
-								searchResults.map((item, index) => {
-									if (!item || !item?.title?.length) {
-										return null;
-									}
-
-									const isSelected = selectedItem === index + 1;
-
+							{isPending && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
+							{hasNoResults && <ContentSearchNoResults />}
+							{hasSearchResults &&
+								searchResults.map((item) => {								
 									const selectItem = () => {
 										handleItemSelection(item);
 									};
-
 									return (
-										<li
-											key={item.id}
-											className={`tenup-content-search-list-item`}
-											style={{
-												marginBottom: '0',
-											}}
-										>
-											{RenderItemComponent ? (
-												<RenderItemComponent
-													item={item}
-													onSelect={selectItem}
-													searchTerm={searchString}
-													contentTypes={contentTypes}
-													isSelected={isSelected}
-													renderType={renderItemType}
-												/>
-											) : (
-												<SearchItem
-													onClick={selectItem}
-													searchTerm={searchString}
-													suggestion={item}
-													contentTypes={contentTypes}
-													isSelected={isSelected}
-													renderType={renderItemType}
-												/>
-											)}
-										</li>
+										<ListItem key={item.id} className="tenup-content-search-list-item">
+											<SearchResultItem
+												item={item}
+												onSelect={selectItem}
+												searchTerm={searchString}
+												contentTypes={contentTypes}
+												renderType={renderItemType}
+											/>
+										</ListItem>
 									);
 								})}
 						</List>
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<string>;
-	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<string>;
-	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<T, R> = Omit<T, keyof R> & 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<IdentifiableObject>;
 }
 
-/**
- * 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<string>;
-  queryFilter: (queryString: string, options: {
-	perPage: number;
-	page: number;
-	contentTypes: Array<string>;
-	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<IdentifiableObject>;
 }
 
 /**
  * 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<WP_REST_API_Post, { title: string }>;
-				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<typeof normalizeResults>;
+export type NormalizedSuggestion = NormalizedSuggestions[number];
+
+interface FetchSearchResultsArgs {
+	keyword: string;
+	page: number;
+	mode: ContentSearchMode;
+	perPage: number;
+	contentTypes: Array<string>;
+	queryFilter: QueryFilter;
+	excludeItems: Array<IdentifiableObject>;
+}
+
+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<Response>({
+		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?= <mail@fabian-kaegy.de>
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<RenderItemComponentProps> = ({
@@ -60,7 +66,7 @@ const SearchItem: React.FC<RenderItemComponentProps> = ({
 	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<RenderItemComponentProps> = ({
 
 	return (
 		<Tooltip text={decodeEntities(suggestion.title)}>
-			<ButtonStyled
-				id={id}
-				onClick={onClick}
-				className={`block-editor-link-control__search-item is-entity`}
-				style={{
-					borderRadius: '0',
-					boxSizing: 'border-box',
-				}}
-			>
-				<span className="block-editor-link-control__search-item-header">
-					<span
-						className="block-editor-link-control__search-item-title"
-						style={{
-							paddingRight: !showType ? 0 : undefined,
-						}}
-					>
-						<TextHighlight text={titleContent} highlight={searchTerm} />
-					</span>
-					<span
-						aria-hidden
-						className="block-editor-link-control__search-item-info"
-						style={{
-							paddingRight: !showType ? 0 : undefined,
-						}}
-					>
+			<SearchItemWrapper id={id} onClick={onClick}>
+				<SearchItemHeader>
+					<SearchItemTitle showType={showType}>
+						<StyledTextHighlight text={titleContent} highlight={searchTerm} />
+					</SearchItemTitle>
+					<SearchItemInfo aria-hidden showType={showType}>
 						<Truncate numberOfLines={1} limit={55} ellipsizeMode="middle">
 							{filterURLForDisplay(safeDecodeURI(suggestion.url)) || ''}
 						</Truncate>
-					</span>
-				</span>
+					</SearchItemInfo>
+				</SearchItemHeader>
 				{showType && (
-					<span className="block-editor-link-control__search-item-type">
+					<SearchItemType>
 						{renderType(suggestion)}
-					</span>
+					</SearchItemType>
 				)}
-			</ButtonStyled>
+			</SearchItemWrapper>
 		</Tooltip>
 	);
 };

From cd03b9d7b22665bc9fb194f75922c1399d72e878 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ka=CC=88gy?= <mail@fabian-kaegy.de>
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?= <mail@fabian-kaegy.de>
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<ContentSearchProps> = ({
 	);
 
 	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?= <mail@fabian-kaegy.de>
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<IdentifiableObject>;
+	results: WP_REST_API_User[] | WP_REST_API_Search_Result[];
+	excludeItems: Array<IdentifiableObject>;
 }
 
 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<string>;
-  queryFilter: QueryFilter
+	keyword: string;
+	page: number;
+	mode: ContentSearchMode;
+	perPage: number;
+	contentTypes: Array<string>;
+	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<IdentifiableObject>;
+	mode: ContentSearchMode;
+	results: WP_REST_API_Search_Result[] | WP_REST_API_User[];
+	excludeItems: Array<IdentifiableObject>;
 }
 
 /**
  * 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<IdentifiableObject>;
 }
 
-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<Response>({
 		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?= <mail@fabian-kaegy.de>
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<IdentifiableObject>;
 }
 
-/**
+/*
  * 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?= <mail@fabian-kaegy.de>
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<RenderItemComponentProps> = ({
 
 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<ContentSearchProps> = ({
 		isFetchingNextPage,
 		fetchNextPage,
 		hasNextPage,
-	  } = useInfiniteQuery(
+	} = useInfiniteQuery(
 		{
 			queryKey: [
 				'search',
@@ -159,57 +159,57 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 	const isPending = status === 'pending';
 
 	return (
-			<StyledNavigableMenu ref={mergedRef} orientation="vertical">
-				<StyledSearchControl
-					value={searchString}
-					onChange={(newSearchString) => {
-						setSearchString(newSearchString);
-					}}
-					label={label}
-					hideLabelFromVision={hideLabelFromVision}
-					placeholder={placeholder}
-					autoComplete="off"
-					onFocus={() => {
-						setIsFocused(true);
-					}}
-				/>
-
-				{hasSearchString || hasInitialResults ? (
-					<>
-						<List className={`tenup-content-search-list`}>
-							{isPending && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
-							{hasNoResults && <ContentSearchNoResults />}
-							{hasSearchResults &&
-								searchResults.map((item) => {								
-									const selectItem = () => {
-										handleItemSelection(item);
-									};
-									return (
-										<ListItem key={item.id} className="tenup-content-search-list-item">
-											<SearchResultItem
-												item={item}
-												onSelect={selectItem}
-												searchTerm={searchString}
-												contentTypes={contentTypes}
-												renderType={renderItemType}
-											/>
-										</ListItem>
-									);
-								})}
-						</List>
-
-						{hasSearchResults && hasNextPage && (
-							<LoadingContainer>
-								<Button onClick={() => fetchNextPage()} variant="secondary">
-									{__('Load more', '10up-block-components')}
-								</Button>
-							</LoadingContainer>
-						)}
-
-						{isFetchingNextPage && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
-					</>
-				) : null}
-			</StyledNavigableMenu>
+		<StyledNavigableMenu ref={mergedRef} orientation="vertical">
+			<StyledSearchControl
+				value={searchString}
+				onChange={(newSearchString) => {
+					setSearchString(newSearchString);
+				}}
+				label={label}
+				hideLabelFromVision={hideLabelFromVision}
+				placeholder={placeholder}
+				autoComplete="off"
+				onFocus={() => {
+					setIsFocused(true);
+				}}
+			/>
+
+			{hasSearchString || hasInitialResults ? (
+				<>
+					<List className={`tenup-content-search-list`}>
+						{isPending && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
+						{hasNoResults && <ContentSearchNoResults />}
+						{hasSearchResults &&
+							searchResults.map((item) => {
+								const selectItem = () => {
+									handleItemSelection(item);
+								};
+								return (
+									<ListItem key={item.id} className="tenup-content-search-list-item">
+										<SearchResultItem
+											item={item}
+											onSelect={selectItem}
+											searchTerm={searchString}
+											contentTypes={contentTypes}
+											renderType={renderItemType}
+										/>
+									</ListItem>
+								);
+							})}
+					</List>
+
+					{hasSearchResults && hasNextPage && (
+						<LoadingContainer>
+							<Button onClick={() => fetchNextPage()} variant="secondary">
+								{__('Load more', '10up-block-components')}
+							</Button>
+						</LoadingContainer>
+					)}
+
+					{isFetchingNextPage && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
+				</>
+			) : null}
+		</StyledNavigableMenu>
 	);
 };
 
@@ -223,4 +223,4 @@ const ContentSearchWrapper: React.FC<ContentSearchProps> = (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?= <mail@fabian-kaegy.de>
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<RenderItemComponentProps> = ({
 	item: suggestion,
 	onSelect: onClick,
@@ -85,27 +98,10 @@ const SearchItem: React.FC<RenderItemComponentProps> = ({
 						</Truncate>
 					</SearchItemInfo>
 				</SearchItemHeader>
-				{showType && (
-					<SearchItemType>
-						{renderType(suggestion)}
-					</SearchItemType>
-				)}
+				{showType && <SearchItemType>{renderType(suggestion)}</SearchItemType>}
 			</SearchItemWrapper>
 		</Tooltip>
 	);
 };
 
-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<ContentSearchProps> = ({
 
 	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<ContentSearchProps> = ({
 		<StyledNavigableMenu ref={mergedRef} orientation="vertical">
 			<StyledSearchControl
 				value={searchString}
-				onChange={(newSearchString) => {
+				onChange={(newSearchString: string) => {
 					setSearchString(newSearchString);
 				}}
 				label={label}
@@ -176,8 +162,8 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 
 			{hasSearchString || hasInitialResults ? (
 				<>
-					<List className={`tenup-content-search-list`}>
-						{isPending && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
+					<List className="tenup-content-search-list">
+						{isPending && <StyledSpinner />}
 						{hasNoResults && <ContentSearchNoResults />}
 						{hasSearchResults &&
 							searchResults.map((item) => {
@@ -185,7 +171,10 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 									handleItemSelection(item);
 								};
 								return (
-									<ListItem key={item.id} className="tenup-content-search-list-item">
+									<ListItem
+										key={item.id}
+										className="tenup-content-search-list-item"
+									>
 										<SearchResultItem
 											item={item}
 											onSelect={selectItem}
@@ -206,7 +195,7 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
 						</LoadingContainer>
 					)}
 
-					{isFetchingNextPage && <StyledSpinner onPointerEnterCapture={null} onPointerLeaveCapture={null} />}
+					{isFetchingNextPage && <StyledSpinner />}
 				</>
 			) : null}
 		</StyledNavigableMenu>