Repository: RocketChat/Rocket.Chat
Pull request #35874: refactor: Omnichannel's tag autocompletes to useInfiniteQuery
Author: login: aleksandernsilva
Labels: name: stat: QA assured
description: Means it has been tested and approved by a company insider
Created: 2025-04-24T19:37:25Z | Merged: 2025-05-19T14:34:39Z

## Proposed changes (including videos or screenshots)
This PR refactors the `useTagsList` hook to take advantage of react query's useInfiniteQuery instead of `RecordList` in combination with `useScrollableRecordList`. 

## Issue(s)
[CTZ-105](https://rocketchat.atlassian.net/browse/CTZ-105)

## Steps to test or reproduce
<!-- Mention how you would reproduce the bug if not mentioned on the issue page already. Also mention which screens are going to have the changes if applicable -->

## Further comments
This change can be tested by visiting pages using tag autocompletes and confirming the expected behavior: fetching, filtering and lazy loading.
- Current chats page


---

This pull request refactors the Omnichannel's tag autocomplete functionality in the Rocket.Chat repository. The main changes include:

1. **New Custom Hook**: Introduces a `useTagsList` hook in TypeScript using `@tanstack/react-query`. This hook fetches and manages a paginated list of livechat tags from the `/v1/livechat/tags` endpoint, supporting filtering by text, department, and an option to view all tags. It also handles infinite scrolling logic.

2. **Component Refactor**: Updates the `AutoCompleteTagsMultiple` component to use the new `useTagsList` hook, simplifying data fetching and pagination logic. It enhances prop handling by deriving types from the `PaginatedMultiSelectFiltered` component and allowing pass-through props.

3. **Department Editing Logic**: Modifies the logic in the department editing form to determine unit requirements by replacing `list.itemCount` with `list.length`.

These changes aim to improve the efficiency and maintainability of the tag autocomplete feature in the Omnichannel module.

[CTZ-105]: https://rocketchat.atlassian.net/browse/CTZ-105?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/apps/meteor/client/components/Omnichannel/hooks/useTagsList.spec.ts b/apps/meteor/client/components/Omnichannel/hooks/useTagsList.spec.ts
new file mode 100644
index 0000000000000..e7df058a241db
--- /dev/null
+++ b/apps/meteor/client/components/Omnichannel/hooks/useTagsList.spec.ts
@@ -0,0 +1,51 @@
+import type { ILivechatTag, Serialized } from '@rocket.chat/core-typings';
+import { MockedAppRootBuilder } from '@rocket.chat/mock-providers/dist/MockedAppRootBuilder';
+import { act, renderHook, waitFor } from '@testing-library/react';
+
+import type { TagListItem } from './useTagsList';
+import { useTagsList } from './useTagsList';
+import { createFakeTag } from '../../../../tests/mocks/data';
+
+const formatTagItem = (tag: Serialized<ILivechatTag>): TagListItem => ({
+	_id: tag._id,
+	label: tag.name,
+	value: tag.name,
+});
+
+const mockGetTags = jest.fn();
+
+const appRoot = new MockedAppRootBuilder().withEndpoint('GET', '/v1/livechat/tags', mockGetTags);
+
+afterEach(() => {
+	jest.clearAllMocks();
+});
+
+it('should fetch tags', async () => {
+	const limit = 5;
+
+	const data = Array.from({ length: 10 }, () => createFakeTag());
+
+	mockGetTags.mockImplementation(({ offset, count }: { offset: number; count: number }) => {
+		const tags = data.slice(offset, offset + count);
+
+		return {
+			tags,
+			count,
+			offset,
+			total: data.length,
+		};
+	});
+
+	const { result } = renderHook(() => useTagsList({ filter: '', limit }), { wrapper: appRoot.build() });
+
+	await waitFor(() => expect(result.current.data).toEqual(data.slice(0, 5).map(formatTagItem)));
+
+	await act(() => result.current.fetchNextPage());
+
+	await waitFor(() => expect(result.current.data).toEqual(data.map(formatTagItem)));
+
+	await act(() => result.current.fetchNextPage());
+
+	// should not fetch again since total was reached
+	expect(mockGetTags).toHaveBeenCalledTimes(2);
+});
diff --git a/apps/meteor/client/components/Omnichannel/hooks/useTagsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useTagsList.ts
new file mode 100644
index 0000000000000..63508c02327df
--- /dev/null
+++ b/apps/meteor/client/components/Omnichannel/hooks/useTagsList.ts
@@ -0,0 +1,59 @@
+import type { ILivechatTag, Serialized } from '@rocket.chat/core-typings';
+import { useEndpoint } from '@rocket.chat/ui-contexts';
+import { useInfiniteQuery } from '@tanstack/react-query';
+
+type TagsListOptions = {
+	filter: string;
+	department?: string;
+	viewAll?: boolean;
+	limit?: number;
+};
+
+export type TagListItem = {
+	_id: string;
+	label: string;
+	value: string;
+};
+
+const DEFAULT_QUERY_LIMIT = 25;
+
+export const useTagsList = (options: TagsListOptions) => {
+	const { viewAll, department, filter, limit = DEFAULT_QUERY_LIMIT } = options;
+
+	const getTags = useEndpoint('GET', '/v1/livechat/tags');
+
+	const formatTagItem = (tag: Serialized<ILivechatTag>): TagListItem => ({
+		_id: tag._id,
+		label: tag.name,
+		value: tag.name,
+	});
+
+	return useInfiniteQuery({
+		queryKey: ['/v1/livechat/tags', options],
+		queryFn: async ({ pageParam: offset = 0 }) => {
+			const { tags, ...data } = await getTags({
+				text: filter,
+				offset,
+				count: limit,
+				...(viewAll && { viewAll: 'true' }),
+				...(department && { department }),
+				sort: JSON.stringify({ name: 1 }),
+			});
+
+			return {
+				...data,
+				tags: tags.map(formatTagItem),
+			};
+		},
+		select: (data) => data.pages.flatMap<TagListItem>((page) => page.tags),
+		initialPageParam: 0,
+		getNextPageParam: (lastPage) => {
+			const offset = lastPage.offset + lastPage.count;
+			return offset < lastPage.total ? offset : undefined;
+		},
+		initialData: () => ({
+			pages: [{ tags: [], offset: 0, count: 0, total: Infinity }],
+			pageParams: [0],
+		}),
+	});
+};
diff --git a/apps/meteor/client/hooks/useTagsList.ts b/apps/meteor/client/hooks/useTagsList.ts
deleted file mode 100644
index 5857193b96779..0000000000000
--- a/apps/meteor/client/hooks/useTagsList.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { useEndpoint } from '@rocket.chat/ui-contexts';
-import { useCallback, useState } from 'react';
-
-import { useScrollableRecordList } from './lists/useScrollableRecordList';
-import { useComponentDidUpdate } from './useComponentDidUpdate';
-import { RecordList } from '../lib/lists/RecordList';
-
-type TagsListOptions = {
-	filter: string;
-	department?: string;
-	viewAll?: boolean;
-};
-
-type TagListItem = { _id: string; label: string; value: string; _updatedAt: Date };
-
-type UseTagsListResult = {
-	itemsList: RecordList<TagListItem>;
-	initialItemCount: number;
-	reload: () => void;
-	loadMoreItems: (start: number, end: number) => void;
-};
-
-export const useTagsList = (options: TagsListOptions): UseTagsListResult => {
-	const { viewAll, department, filter } = options;
-	const [itemsList, setItemsList] = useState(() => new RecordList<TagListItem>());
-	const reload = useCallback(() => setItemsList(new RecordList<TagListItem>()), []);
-
-	const getTags = useEndpoint('GET', '/v1/livechat/tags');
-
-	useComponentDidUpdate(() => {
-		options && reload();
-	}, [options, reload]);
-
-	const fetchData = useCallback(
-		async (start: number, end: number) => {
-			const { tags, total } = await getTags({
-				text: filter,
-				offset: start,
-				count: end + start,
-				...(viewAll && { viewAll: 'true' }),
-				...(department && { department }),
-				sort: JSON.stringify({ name: 1 }),
-			});
-
-			return {
-				items: tags.map<any>((tag: any) => ({
-					_id: tag._id,
-					label: tag.name,
-					value: tag.name,
-				})),
-				itemCount: total,
-			};
-		},
-		[getTags, filter, viewAll, department],
-	);
-
-	const { loadMoreItems, initialItemCount } = useScrollableRecordList(itemsList, fetchData, 25);
-
-	return {
-		reload,
-		itemsList,
-		loadMoreItems,
-		initialItemCount,
-	};
-};
diff --git a/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx b/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx
index 6a980e6de6bfb..88370a0006609 100644
--- a/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx
+++ b/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx
@@ -1,43 +1,37 @@
-import type { PaginatedMultiSelectOption } from '@rocket.chat/fuselage';
 import { PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage';
 import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
+import type { ComponentProps } from 'react';
 import { memo, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import { useRecordList } from '../../hooks/lists/useRecordList';
-import { AsyncStatePhase } from '../../hooks/useAsyncState';
-import { useTagsList } from '../../hooks/useTagsList';
+import { useTagsList } from '../../components/Omnichannel/hooks/useTagsList';
 
-type AutoCompleteTagsMultipleProps = {
-	id?: string;
-	value?: PaginatedMultiSelectOption[];
-	onlyMyTags?: boolean;
-	onChange?: (value: PaginatedMultiSelectOption[]) => void;
+type AutoCompleteTagsMultipleProps = Omit<
+	ComponentProps<typeof PaginatedMultiSelectFiltered>,
+	'filter' | 'setFilter' | 'options' | 'endReached' | 'renderItem'
+> & {
 	department?: string;
 	viewAll?: boolean;
 };
 
 const AutoCompleteTagsMultiple = ({
-	id,
 	value = [],
-	onlyMyTags = false,
 	onChange = () => undefined,
 	department,
 	viewAll = false,
+	placeholder,
+	...props
 }: AutoCompleteTagsMultipleProps) => {
 	const { t } = useTranslation();
 	const [tagsFilter, setTagsFilter] = useState('');
 
 	const debouncedTagsFilter = useDebouncedValue(tagsFilter, 500);
 
-	const { itemsList: tagsList, loadMoreItems: loadMoreTags } = useTagsList(
-		useMemo(
-			() => ({ filter: debouncedTagsFilter, onlyMyTags, department, viewAll }),
-			[debouncedTagsFilter, onlyMyTags, department, viewAll],
-		),
-	);
-
-	const { phase: tagsPhase, items: tagsItems, itemCount: tagsTotal } = useRecordList(tagsList);
+	const { data: tagsItems, fetchNextPage } = useTagsList({
+		filter: debouncedTagsFilter,
+		department,
+		viewAll,
+	});
 
 	const tagsOptions = useMemo(() => {
 		const pending = value.filter(({ value }) => !tagsItems.find((tag) => tag.value === value));
@@ -46,8 +40,8 @@ const AutoCompleteTagsMultiple = ({
 
 	return (
 		<PaginatedMultiSelectFiltered
-			id={id}
 			withTitle
+			{...props}
 			value={value}
 			onChange={onChange}
 			filter={tagsFilter}
@@ -56,10 +50,8 @@ const AutoCompleteTagsMultiple = ({
 			width='100%'
 			flexShrink={0}
 			flexGrow={0}
-			placeholder={t('Select_an_option')}
-			endReached={
-				tagsPhase === AsyncStatePhase.LOADING ? () => undefined : (start) => start && loadMoreTags(start, Math.min(50, tagsTotal))
-			}
+			placeholder={placeholder ?? t('Select_an_option')}
+			endReached={() => fetchNextPage()}
 		/>
 	);
 };
diff --git a/apps/meteor/tests/mocks/data.ts b/apps/meteor/tests/mocks/data.ts
index 0343bf7012440..4c4090e3b1323 100644
--- a/apps/meteor/tests/mocks/data.ts
+++ b/apps/meteor/tests/mocks/data.ts
@@ -12,6 +12,7 @@ import type {
 	ILivechatContactChannel,
 	Serialized,
 	ILivechatAgent,
+	ILivechatTag,
 } from '@rocket.chat/core-typings';
 import { parse } from '@rocket.chat/message-parser';
 
@@ -363,3 +364,14 @@ export function createFakeAgent(overrides?: Partial<Serialized<ILivechatAgent>>)
 		...overrides,
 	};
 }
+
+export function createFakeTag(overrides?: Partial<Serialized<ILivechatTag>>): Serialized<ILivechatTag> {
+	return {
+		_id: faker.string.uuid(),
+		name: faker.commerce.department(),
+		description: 'description',
+		numDepartments: 0,
+		departments: [],
+		...overrides,
+	};
+}


Reviews:
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-24T19:39:19Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-24T19:39:26Z
user: login: tassoevan
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-13T17:40:38Z
user: login: aleksandernsilva
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:06:16Z
user: login: tassoevan
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-15T20:18:03Z

Review comments:
diff_hunk: @@ -0,0 +1,59 @@
+import type { ILivechatTag, Serialized } from '@rocket.chat/core-typings';
+import { useEndpoint } from '@rocket.chat/ui-contexts';
+import { useInfiniteQuery } from '@tanstack/react-query';
+
+type TagsListOptions = {
+	filter: string;
+	department?: string;
+	viewAll?: boolean;
+	limit?: number;
+};
+
+type TagListItem = {
+	_id: string;
+	label: string;
+	value: string;
+};
+
+const DEFAULT_QUERY_LIMIT = 25;
+
+export const useTagsList = (options: TagsListOptions) => {
+	const { viewAll, department, filter, limit } = options;
+
+	const getTags = useEndpoint('GET', '/v1/livechat/tags');
+
+	const formatTagItem = (tag: Serialized<ILivechatTag>): TagListItem => ({
+		_id: tag._id,
+		label: tag.name,
+		value: tag.name,
+	});
+
+	return useInfiniteQuery({
+		queryKey: ['/v1/livechat/tags', options],
+		queryFn: async ({ pageParam: offset = 0 }) => {
+			const { tags, ...data } = await getTags({
+				text: filter,
+				offset,
+				count: limit ?? DEFAULT_QUERY_LIMIT,
path: apps/meteor/client/components/Omnichannel/hooks/useTagsList.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Maintainability](https://img.shields.io/badge/Maintainability-0091EA) ![medium](https://img.shields.io/badge/severity_level-medium-1A7BBE)
```typescript
// Near line 21
const effectiveLimit = limit ?? DEFAULT_QUERY_LIMIT;

// ... later in queryFn (line 37)
count: effectiveLimit,

// ... later in initialData (line 55)
count: effectiveLimit,
```
The expression `limit ?? DEFAULT_QUERY_LIMIT` is repeated in multiple locations within the `useTagsList` hook, leading to code duplication and reduced maintainability.

This issue appears in multiple locations:
* apps/meteor/client/components/Omnichannel/hooks/useTagsList.ts: Lines 37-37
* apps/meteor/client/components/Omnichannel/hooks/useTagsList.ts: Lines 37-37
Please calculate the effective limit once at the beginning of the hook and store it in a variable (e.g., `effectiveLimit`) to avoid repetition and improve maintainability.


<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-24T19:39:18Z
author_association: NONE
diff_hunk: @@ -1,43 +1,37 @@
-import type { PaginatedMultiSelectOption } from '@rocket.chat/fuselage';
 import { PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage';
 import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
+import type { ComponentProps } from 'react';
 import { memo, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import { useRecordList } from '../../hooks/lists/useRecordList';
-import { AsyncStatePhase } from '../../hooks/useAsyncState';
-import { useTagsList } from '../../hooks/useTagsList';
+import { useTagsList } from '../../components/Omnichannel/hooks/useTagsList';
 
-type AutoCompleteTagsMultipleProps = {
-	id?: string;
-	value?: PaginatedMultiSelectOption[];
-	onlyMyTags?: boolean;
-	onChange?: (value: PaginatedMultiSelectOption[]) => void;
+type AutoCompleteTagsMultipleProps = Omit<
+	ComponentProps<typeof PaginatedMultiSelectFiltered>,
+	'filter' | 'setFilter' | 'options' | 'endReached' | 'renderItem'
+> & {
 	department?: string;
 	viewAll?: boolean;
 };
 
 const AutoCompleteTagsMultiple = ({
-	id,
 	value = [],
-	onlyMyTags = false,
 	onChange = () => undefined,
 	department,
 	viewAll = false,
+	placeholder,
+	...props
 }: AutoCompleteTagsMultipleProps) => {
 	const { t } = useTranslation();
 	const [tagsFilter, setTagsFilter] = useState('');
 
 	const debouncedTagsFilter = useDebouncedValue(tagsFilter, 500);
 
-	const { itemsList: tagsList, loadMoreItems: loadMoreTags } = useTagsList(
-		useMemo(
-			() => ({ filter: debouncedTagsFilter, onlyMyTags, department, viewAll }),
-			[debouncedTagsFilter, onlyMyTags, department, viewAll],
-		),
-	);
-
-	const { phase: tagsPhase, items: tagsItems, itemCount: tagsTotal } = useRecordList(tagsList);
+	const { data: tagsItems, fetchNextPage } = useTagsList({
+		filter: debouncedTagsFilter,
+		department,
+		viewAll,
+	});
path: apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Error Handling](https://img.shields.io/badge/Error_Handling-FF6D00) ![high](https://img.shields.io/badge/severity_level-high-6B6B92)
```typescript
const tagsQuery = useTagsList({
		filter: debouncedTagsFilter,
		department,
		viewAll,
	});

const { data: tagsItems, fetchNextPage, isLoading, error } = tagsQuery; // Example assuming useTagsList returns loading/error states

// Later in the component:
if (tagsQuery.isLoading) {
  return <p>Loading...</p>;
}
if (tagsQuery.error) {
  // Display error message
  return <p>Error: {tagsQuery.error.message}</p>;
}
if (!tagsQuery.data) {
  return <p>No data available</p>;
}

// ... rest of the component using tagsItems
```
The `useTagsList` hook is destructured for `data` and `fetchNextPage`, but potential loading states or errors from the hook are not explicitly handled in this component. Consider whether loading indicators or error messages should be displayed to the user, potentially by exposing and using `isLoading` or `error` states from the `useTagsList` hook if available.  Also, ensure that you handle the case where data might be undefined, especially after checking loading states, as TypeScript can't always infer data availability in these scenarios.

<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-24T19:39:26Z
author_association: NONE
diff_hunk: @@ -0,0 +1,57 @@
+import type { ILivechatTag, Serialized } from '@rocket.chat/core-typings';
+import { MockedAppRootBuilder } from '@rocket.chat/mock-providers/dist/MockedAppRootBuilder';
+import { act, renderHook, waitFor } from '@testing-library/react';
+
+import type { TagListItem } from './useTagsList';
+import { useTagsList } from './useTagsList';
+import { createFakeTag } from '../../../../tests/mocks/data';
+
+const formatTagItem = (tag: Serialized<ILivechatTag>): TagListItem => ({
+	_id: tag._id,
+	label: tag.name,
+	value: tag.name,
+});
+
+const mockGetTags = jest.fn();
+
+const appRoot = new MockedAppRootBuilder().withEndpoint('GET', '/v1/livechat/tags', mockGetTags);
+
+afterEach(() => {
+	jest.clearAllMocks();
+});
+
+it('should fetch tags', async () => {
+	const limit = 5;
+
+	const data = Array.from({ length: 10 }, () => createFakeTag());
+
+	mockGetTags.mockImplementation(({ offset, count }: { offset: number; count: number }) => {
+		const tags = data.slice(offset, offset + count);
+
+		return {
+			tags,
+			count,
+			offset,
+			total: data.length,
+		};
+	});
+
+	const { result } = renderHook(() => useTagsList({ filter: '', limit }), { wrapper: appRoot.build() });
+
+	expect(result.current.isFetching).toBe(true);
+	await waitFor(() => expect(result.current.isFetching).toBe(false));
path: apps/meteor/client/components/Omnichannel/hooks/useTagsList.spec.ts
user: login: tassoevan
body: There is no need to assert the transient state.
created_at: 2025-05-13T17:35:38Z
author_association: MEMBER
diff_hunk: @@ -1,43 +1,37 @@
-import type { PaginatedMultiSelectOption } from '@rocket.chat/fuselage';
 import { PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage';
 import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
+import type { ComponentProps } from 'react';
 import { memo, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import { useRecordList } from '../../hooks/lists/useRecordList';
-import { AsyncStatePhase } from '../../hooks/useAsyncState';
-import { useTagsList } from '../../hooks/useTagsList';
+import { useTagsList } from '../../components/Omnichannel/hooks/useTagsList';
 
-type AutoCompleteTagsMultipleProps = {
-	id?: string;
-	value?: PaginatedMultiSelectOption[];
-	onlyMyTags?: boolean;
-	onChange?: (value: PaginatedMultiSelectOption[]) => void;
+type AutoCompleteTagsMultipleProps = Omit<
+	ComponentProps<typeof PaginatedMultiSelectFiltered>,
+	'filter' | 'setFilter' | 'options' | 'endReached' | 'renderItem'
+> & {
 	department?: string;
 	viewAll?: boolean;
 };
path: apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx
user: login: tassoevan
body: Why more props available for the component instead of keeping it restricted?
created_at: 2025-05-13T17:39:58Z
author_association: MEMBER
diff_hunk: @@ -1,43 +1,37 @@
-import type { PaginatedMultiSelectOption } from '@rocket.chat/fuselage';
 import { PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage';
 import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
+import type { ComponentProps } from 'react';
 import { memo, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import { useRecordList } from '../../hooks/lists/useRecordList';
-import { AsyncStatePhase } from '../../hooks/useAsyncState';
-import { useTagsList } from '../../hooks/useTagsList';
+import { useTagsList } from '../../components/Omnichannel/hooks/useTagsList';
 
-type AutoCompleteTagsMultipleProps = {
-	id?: string;
-	value?: PaginatedMultiSelectOption[];
-	onlyMyTags?: boolean;
-	onChange?: (value: PaginatedMultiSelectOption[]) => void;
+type AutoCompleteTagsMultipleProps = Omit<
+	ComponentProps<typeof PaginatedMultiSelectFiltered>,
+	'filter' | 'setFilter' | 'options' | 'endReached' | 'renderItem'
+> & {
 	department?: string;
 	viewAll?: boolean;
 };
path: apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx
user: login: aleksandernsilva
body: My idea here was to allow the same level of customization the underlying component provides, omitting only the properties that would compromise the behavior that this autocomplete specialization is trying to facilitate. 
created_at: 2025-05-13T19:06:16Z
author_association: CONTRIBUTOR

PR comments:
user: login: dionisio-bot[bot]
created_at: 2025-04-24T19:37:29Z
body: Looks like this PR is ready to merge! 🎉
If you have any trouble, please check the [PR guidelines](https://handbook.rocket.chat/departments-and-operations/research-and-development/engineering/development/pr-general-instructions-and-handling)
author_association: CONTRIBUTOR
user: login: changeset-bot[bot]
created_at: 2025-04-24T19:37:29Z
body: ###  ⚠️  No Changeset found

Latest commit: 841f8ea9460efa7aa1afd397fb0b4af6ab44a089

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/RocketChat/Rocket.Chat/new/refactor/tag-autocomplete-query?filename=.changeset/afraid-ghosts-fry.md&value=---%0A%22%40rocket.chat%2Fmeteor%22%3A%20patch%0A---%0A%0Arefactor%3A%20Omnichannel's%20tag%20autocompletes%20to%20useInfiniteQuery%0A)


author_association: NONE
user: login: kody-ai[bot]
created_at: 2025-04-24T19:37:30Z
body: ## Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.



<details>
<summary>Kody Guide: Usage and Configuration</summary>

<details>
<summary>Interacting with Kody</summary>

- **Request a Review:** Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

- **Provide Feedback:** Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

</details>

<details>
<summary>Current Kody Configuration</summary>

<details>
<summary>Review Options</summary>

The following review options are enabled or disabled:

| Options                        | Enabled |
|-------------------------------|---------|
| **Security** | ✅ |
| **Code Style** | ✅ |
| **Kody Rules** | ✅ |
| **Refactoring** | ✅ |
| **Error Handling** | ✅ |
| **Maintainability** | ✅ |
| **Potential Issues** | ✅ |
| **Documentation And Comments** | ✅ |
| **Performance And Optimization** | ✅ |
| **Breaking Changes** | ❌ |

</details>

**[Access your configuration settings here.](https://app.kodus.io/settings/code-review/global/general)**

</details>
</details>

<!-- kody-codereview -->
&#8203;
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-24T19:52:09Z
body: [PR Preview Action](https://github.com/rossjrw/pr-preview-action) v1.6.1
:---:
| <p></p> :rocket: View preview at <br> https://RocketChat.github.io/Rocket.Chat/pr-preview/pr-35874/ <br><br>
| <h6>Built to branch [`gh-pages`](https://github.com/RocketChat/Rocket.Chat/tree/gh-pages) at 2025-05-19 13:15 UTC. <br> Preview will be ready when the [GitHub Pages deployment](https://github.com/RocketChat/Rocket.Chat/deployments) is complete. <br><br> </h6>
<!-- Sticky Pull Request Commentpr-preview -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-24T19:53:07Z
body: ## [Codecov](https://app.codecov.io/gh/RocketChat/Rocket.Chat/pull/35874?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat) Report
Attention: Patch coverage is `87.83784%` with `9 lines` in your changes missing coverage. Please review.
> Project coverage is 64.96%. Comparing base [(`29bf978`)](https://app.codecov.io/gh/RocketChat/Rocket.Chat/commit/29bf9785f75826f4b11afa0d1b4d2ace6c9feab4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat) to head [(`841f8ea`)](https://app.codecov.io/gh/RocketChat/Rocket.Chat/commit/841f8ea9460efa7aa1afd397fb0b4af6ab44a089?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat).
> Report is 1 commits behind head on develop.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/RocketChat/Rocket.Chat/pull/35874/graphs/tree.svg?width=650&height=150&src=pr&token=lEAH159Fca&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat)](https://app.codecov.io/gh/RocketChat/Rocket.Chat/pull/35874?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat)

```diff
@@             Coverage Diff             @@
##           develop   #35874      +/-   ##
===========================================
+ Coverage    64.92%   64.96%   +0.03%     
===========================================
  Files         3100     3100              
  Lines        92490    92544      +54     
  Branches     17652    17661       +9     
===========================================
+ Hits         60046    60117      +71     
+ Misses       29677    29665      -12     
+ Partials      2767     2762       -5     
```

| [Flag](https://app.codecov.io/gh/RocketChat/Rocket.Chat/pull/35874/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat) | Coverage Δ | |
|---|---|---|
| [e2e](https://app.codecov.io/gh/RocketChat/Rocket.Chat/pull/35874/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat) | `58.45% <88.88%> (+0.01%)` | :arrow_up: |
| [unit](https://app.codecov.io/gh/RocketChat/Rocket.Chat/pull/35874/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat) | `72.10% <87.32%> (+0.07%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RocketChat#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: kody-ai[bot]
created_at: 2025-04-25T15:32:47Z
body: # Kody Review Complete
**Great news!** 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

<details>
<summary>Kody Guide: Usage and Configuration</summary>

<details>
<summary>Interacting with Kody</summary>

- **Request a Review:** Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

- **Provide Feedback:** Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

</details>

<details>
<summary>Current Kody Configuration</summary>

<details>
<summary>Review Options</summary>

The following review options are enabled or disabled:

| Options                        | Enabled |
|-------------------------------|---------|
| **Security** | ✅ |
| **Code Style** | ✅ |
| **Kody Rules** | ✅ |
| **Refactoring** | ✅ |
| **Error Handling** | ✅ |
| **Maintainability** | ✅ |
| **Potential Issues** | ✅ |
| **Documentation And Comments** | ✅ |
| **Performance And Optimization** | ✅ |
| **Breaking Changes** | ❌ |

</details>

**[Access your configuration settings here.](https://app.kodus.io/settings/code-review/global/general)**

</details>
</details>

<!-- kody-codereview -->
&#8203;
author_association: NONE
user: login: kody-ai[bot]
created_at: 2025-05-07T17:42:13Z
body: # Kody Review Complete
**Great news!** 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

<details>
<summary>Kody Guide: Usage and Configuration</summary>

<details>
<summary>Interacting with Kody</summary>

- **Request a Review:** Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

- **Provide Feedback:** Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

</details>

<details>
<summary>Current Kody Configuration</summary>

<details>
<summary>Review Options</summary>

The following review options are enabled or disabled:

| Options                        | Enabled |
|-------------------------------|---------|
| **Security** | ✅ |
| **Code Style** | ✅ |
| **Kody Rules** | ✅ |
| **Refactoring** | ✅ |
| **Error Handling** | ✅ |
| **Maintainability** | ✅ |
| **Potential Issues** | ✅ |
| **Documentation And Comments** | ✅ |
| **Performance And Optimization** | ✅ |
| **Breaking Changes** | ❌ |

</details>

**[Access your configuration settings here.](https://app.kodus.io/settings/code-review/global/general)**

</details>
</details>

<!-- kody-codereview -->
&#8203;
author_association: NONE
user: login: kody-ai[bot]
created_at: 2025-05-13T20:54:12Z
body: # Kody Review Complete
**Great news!** 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

<details>
<summary>Kody Guide: Usage and Configuration</summary>

<details>
<summary>Interacting with Kody</summary>

- **Request a Review:** Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

- **Provide Feedback:** Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

</details>

<details>
<summary>Current Kody Configuration</summary>

<details>
<summary>Review Options</summary>

The following review options are enabled or disabled:

| Options                        | Enabled |
|-------------------------------|---------|
| **Security** | ✅ |
| **Code Style** | ✅ |
| **Kody Rules** | ✅ |
| **Refactoring** | ✅ |
| **Error Handling** | ✅ |
| **Maintainability** | ✅ |
| **Potential Issues** | ✅ |
| **Documentation And Comments** | ✅ |
| **Performance And Optimization** | ✅ |
| **Breaking Changes** | ❌ |

</details>

**[Access your configuration settings here.](https://app.kodus.io/settings/code-review/global/general)**

</details>
</details>

<!-- kody-codereview -->
&#8203;
author_association: NONE
user: login: kodiakhq[bot]
created_at: 2025-05-19T07:09:23Z
body: 
This PR currently has a merge conflict. Please resolve this and then re-add the `['stat: ready to merge', 'automerge']` label.

author_association: CONTRIBUTOR