mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-06 06:16:21 +00:00
fix(ui): infinite scroll for tag listing inside term boost (#21776)
* fix: infinite scroll for tag listing for term boost * revert AsyncSelectList changes * fix term boost query call * fix async select for infinite scroll and add tests * address pr comments * minor refactor --------- Co-authored-by: Shailesh Parmar <shailesh.parmar.webdev@gmail.com>
This commit is contained in:
parent
27a05b17b0
commit
d98c762501
@ -48,6 +48,9 @@ const mockTagResponse = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
paging: {
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('TermBoost Component', () => {
|
describe('TermBoost Component', () => {
|
||||||
@ -72,10 +75,13 @@ describe('TermBoost Component', () => {
|
|||||||
fireEvent.mouseDown(select);
|
fireEvent.mouseDown(select);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(tagClassBase.getTags).toHaveBeenCalled();
|
expect(tagClassBase.getTags).toHaveBeenCalledWith('', 1, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText('PII.Sensitive')).toBeInTheDocument();
|
// Wait for the async select to populate options
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('PII.Sensitive')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should handle delete tag boost', () => {
|
it('Should handle delete tag boost', () => {
|
||||||
|
@ -58,9 +58,9 @@ const TermBoostComponent: React.FC<TermBoostProps> = ({
|
|||||||
}
|
}
|
||||||
}, [termBoost, isNewBoost]);
|
}, [termBoost, isNewBoost]);
|
||||||
|
|
||||||
const fetchTags = async (searchText: string) => {
|
const fetchTags = async (searchText: string, page = 1) => {
|
||||||
try {
|
try {
|
||||||
const response = await tagClassBase.getTags(searchText, 1, true);
|
const response = await tagClassBase.getTags(searchText, page, true);
|
||||||
|
|
||||||
const formattedOptions = response.data.map((item) => {
|
const formattedOptions = response.data.map((item) => {
|
||||||
const fqn = item.data.fullyQualifiedName;
|
const fqn = item.data.fullyQualifiedName;
|
||||||
@ -93,11 +93,19 @@ const TermBoostComponent: React.FC<TermBoostProps> = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return formattedOptions;
|
// Return PagingResponse structure for infinite scroll support
|
||||||
|
return {
|
||||||
|
data: formattedOptions,
|
||||||
|
paging: response.paging,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(error as AxiosError);
|
showErrorToast(error as AxiosError);
|
||||||
|
|
||||||
return [];
|
// Return empty PagingResponse structure on error
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
paging: { total: 0 },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -124,6 +132,7 @@ const TermBoostComponent: React.FC<TermBoostProps> = ({
|
|||||||
<Row className="p-box d-flex flex-column">
|
<Row className="p-box d-flex flex-column">
|
||||||
<Col className="p-y-xs p-l-sm p-r-xss border-radius-card m-b-sm bg-white config-section-content">
|
<Col className="p-y-xs p-l-sm p-r-xss border-radius-card m-b-sm bg-white config-section-content">
|
||||||
<AsyncSelect
|
<AsyncSelect
|
||||||
|
enableInfiniteScroll
|
||||||
showSearch
|
showSearch
|
||||||
api={fetchTags}
|
api={fetchTags}
|
||||||
className="w-full custom-select"
|
className="w-full custom-select"
|
||||||
|
@ -0,0 +1,325 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import { DefaultOptionType } from 'antd/lib/select';
|
||||||
|
import React from 'react';
|
||||||
|
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||||
|
import { AsyncSelect, PagingResponse } from './AsyncSelect';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('../../../utils/ToastUtils', () => ({
|
||||||
|
showErrorToast: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../Loader/Loader', () => {
|
||||||
|
return function MockLoader({ size }: { size?: string }) {
|
||||||
|
return <div data-testid={`loader-${size || 'default'}`}>Loading...</div>;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock debounce to make tests synchronous
|
||||||
|
jest.mock('lodash', () => ({
|
||||||
|
...jest.requireActual('lodash'),
|
||||||
|
debounce: (fn: any) => fn,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockOptions: DefaultOptionType[] = [
|
||||||
|
{ label: 'Option 1', value: 'option1' },
|
||||||
|
{ label: 'Option 2', value: 'option2' },
|
||||||
|
{ label: 'Option 3', value: 'option3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockPaginatedResponse: PagingResponse<DefaultOptionType[]> = {
|
||||||
|
data: mockOptions,
|
||||||
|
paging: {
|
||||||
|
total: 10,
|
||||||
|
after: 'cursor1',
|
||||||
|
before: 'cursor2',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('AsyncSelect Component', () => {
|
||||||
|
const mockApi = jest.fn();
|
||||||
|
const mockOnSelect = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
(showErrorToast as jest.Mock).mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Basic Functionality', () => {
|
||||||
|
it('should render AsyncSelect component', () => {
|
||||||
|
mockApi.mockResolvedValue(mockOptions);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AsyncSelect
|
||||||
|
api={mockApi}
|
||||||
|
data-testid="async-select"
|
||||||
|
placeholder="Select option"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('async-select')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call API on mount with empty search', async () => {
|
||||||
|
mockApi.mockResolvedValue(mockOptions);
|
||||||
|
|
||||||
|
render(<AsyncSelect api={mockApi} data-testid="async-select" />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockApi).toHaveBeenCalledWith('', undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display loading state initially', async () => {
|
||||||
|
mockApi.mockImplementation(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) => setTimeout(() => resolve(mockOptions), 100))
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<AsyncSelect api={mockApi} data-testid="async-select" />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('loader-small')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should populate options after API call', async () => {
|
||||||
|
mockApi.mockResolvedValue(mockOptions);
|
||||||
|
|
||||||
|
render(<AsyncSelect api={mockApi} data-testid="async-select" />);
|
||||||
|
|
||||||
|
fireEvent.mouseDown(screen.getByRole('combobox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option 3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call API with search text when typing', async () => {
|
||||||
|
mockApi.mockResolvedValue(mockOptions);
|
||||||
|
|
||||||
|
render(<AsyncSelect api={mockApi} data-testid="async-select" />);
|
||||||
|
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
fireEvent.change(input, { target: { value: 'test search' } });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockApi).toHaveBeenCalledWith('test search', undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle option selection', async () => {
|
||||||
|
mockApi.mockResolvedValue(mockOptions);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AsyncSelect
|
||||||
|
api={mockApi}
|
||||||
|
data-testid="async-select"
|
||||||
|
onSelect={mockOnSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.mouseDown(screen.getByRole('combobox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Option 1'));
|
||||||
|
|
||||||
|
expect(mockOnSelect).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle API errors gracefully', async () => {
|
||||||
|
const error = new Error('API Error');
|
||||||
|
mockApi.mockRejectedValue(error);
|
||||||
|
|
||||||
|
render(<AsyncSelect api={mockApi} data-testid="async-select" />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(showErrorToast).toHaveBeenCalledWith(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Infinite Scroll Functionality', () => {
|
||||||
|
it('should enable infinite scroll when enableInfiniteScroll is true', async () => {
|
||||||
|
mockApi.mockResolvedValue(mockPaginatedResponse);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AsyncSelect
|
||||||
|
enableInfiniteScroll
|
||||||
|
api={mockApi}
|
||||||
|
data-testid="async-select"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockApi).toHaveBeenCalledWith('', 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle paginated response correctly', async () => {
|
||||||
|
mockApi.mockResolvedValue(mockPaginatedResponse);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AsyncSelect
|
||||||
|
enableInfiniteScroll
|
||||||
|
api={mockApi}
|
||||||
|
data-testid="async-select"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.mouseDown(screen.getByRole('combobox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option 3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not load more when all options are loaded', async () => {
|
||||||
|
const limitedResponse: PagingResponse<DefaultOptionType[]> = {
|
||||||
|
data: mockOptions,
|
||||||
|
paging: { total: 3 }, // Same as data length
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApi.mockResolvedValue(limitedResponse);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AsyncSelect
|
||||||
|
enableInfiniteScroll
|
||||||
|
api={mockApi}
|
||||||
|
data-testid="async-select"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.mouseDown(screen.getByRole('combobox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate scroll to bottom
|
||||||
|
const dropdown = screen.getByRole('listbox');
|
||||||
|
Object.defineProperty(dropdown, 'scrollTop', {
|
||||||
|
value: 100,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(dropdown, 'offsetHeight', {
|
||||||
|
value: 100,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(dropdown, 'scrollHeight', {
|
||||||
|
value: 200,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.scroll(dropdown);
|
||||||
|
|
||||||
|
// Should not call API again since all options are loaded
|
||||||
|
expect(mockApi).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset page when search text changes', async () => {
|
||||||
|
mockApi.mockResolvedValue(mockPaginatedResponse);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AsyncSelect
|
||||||
|
enableInfiniteScroll
|
||||||
|
api={mockApi}
|
||||||
|
data-testid="async-select"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockApi).toHaveBeenCalledWith('', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change search text
|
||||||
|
fireEvent.change(input, { target: { value: 'new search' } });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockApi).toHaveBeenCalledWith('new search', 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Configuration Options', () => {
|
||||||
|
it('should use custom debounce timeout', async () => {
|
||||||
|
mockApi.mockResolvedValue(mockOptions);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AsyncSelect
|
||||||
|
api={mockApi}
|
||||||
|
data-testid="async-select"
|
||||||
|
debounceTimeout={1000}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Since we mocked debounce to be synchronous, this just ensures the prop is passed
|
||||||
|
expect(screen.getByTestId('async-select')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work without infinite scroll by default', async () => {
|
||||||
|
mockApi.mockResolvedValue(mockOptions);
|
||||||
|
|
||||||
|
render(<AsyncSelect api={mockApi} data-testid="async-select" />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockApi).toHaveBeenCalledWith('', undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
it('should handle empty API response', async () => {
|
||||||
|
mockApi.mockResolvedValue([]);
|
||||||
|
|
||||||
|
render(<AsyncSelect api={mockApi} data-testid="async-select" />);
|
||||||
|
|
||||||
|
fireEvent.mouseDown(screen.getByRole('combobox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockApi).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not crash and should show no options
|
||||||
|
expect(screen.queryByText('Option 1')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle scroll events when infinite scroll is disabled', async () => {
|
||||||
|
mockApi.mockResolvedValue(mockOptions);
|
||||||
|
|
||||||
|
render(<AsyncSelect api={mockApi} data-testid="async-select" />);
|
||||||
|
|
||||||
|
fireEvent.mouseDown(screen.getByRole('combobox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate scroll - should not trigger additional API calls
|
||||||
|
const dropdown = screen.getByRole('listbox');
|
||||||
|
fireEvent.scroll(dropdown);
|
||||||
|
|
||||||
|
expect(mockApi).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -13,9 +13,19 @@
|
|||||||
|
|
||||||
import { Select, SelectProps } from 'antd';
|
import { Select, SelectProps } from 'antd';
|
||||||
import { DefaultOptionType } from 'antd/lib/select';
|
import { DefaultOptionType } from 'antd/lib/select';
|
||||||
import { debounce } from 'lodash';
|
import { AxiosError } from 'axios';
|
||||||
|
import { debounce, isObject } from 'lodash';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Paging } from '../../../generated/type/paging';
|
||||||
|
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||||
import Loader from '../Loader/Loader';
|
import Loader from '../Loader/Loader';
|
||||||
|
import { AsyncSelectListProps } from './AsyncSelectList.interface';
|
||||||
|
|
||||||
|
// Interface for paginated API response
|
||||||
|
export interface PagingResponse<T> {
|
||||||
|
data: T;
|
||||||
|
paging: Paging;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AsyncSelect to work with options provided from API directly
|
* AsyncSelect to work with options provided from API directly
|
||||||
@ -27,47 +37,131 @@ import Loader from '../Loader/Loader';
|
|||||||
export const AsyncSelect = ({
|
export const AsyncSelect = ({
|
||||||
options,
|
options,
|
||||||
api,
|
api,
|
||||||
|
enableInfiniteScroll = false,
|
||||||
|
debounceTimeout = 400,
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectProps & {
|
}: SelectProps & AsyncSelectListProps) => {
|
||||||
api: (queryString: string) => Promise<DefaultOptionType[]>;
|
|
||||||
}) => {
|
|
||||||
const [optionsInternal, setOptionsInternal] = useState<DefaultOptionType[]>();
|
const [optionsInternal, setOptionsInternal] = useState<DefaultOptionType[]>();
|
||||||
const [loadingOptions, setLoadingOptions] = useState(false);
|
const [loadingOptions, setLoadingOptions] = useState(false);
|
||||||
|
const [hasContentLoading, setHasContentLoading] = useState(false);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [paging, setPaging] = useState<Paging>({} as Paging);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOptionsInternal(options);
|
setOptionsInternal(options);
|
||||||
}, [options]);
|
}, [options]);
|
||||||
|
|
||||||
const fetchOptions = useCallback(
|
const fetchOptions = useCallback(
|
||||||
debounce((value: string) => {
|
debounce(async (value: string, page = 1) => {
|
||||||
setLoadingOptions(true);
|
if (page === 1) {
|
||||||
api(value).then((res) => {
|
setLoadingOptions(true);
|
||||||
setOptionsInternal(res);
|
setOptionsInternal([]);
|
||||||
|
} else {
|
||||||
|
setHasContentLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api(
|
||||||
|
value,
|
||||||
|
enableInfiniteScroll ? page : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
enableInfiniteScroll &&
|
||||||
|
response &&
|
||||||
|
isObject(response) &&
|
||||||
|
'data' in response
|
||||||
|
) {
|
||||||
|
// Handle paginated response
|
||||||
|
const pagingResponse = response as PagingResponse<
|
||||||
|
DefaultOptionType[]
|
||||||
|
>;
|
||||||
|
if (page === 1) {
|
||||||
|
setOptionsInternal(pagingResponse.data);
|
||||||
|
setPaging(pagingResponse.paging);
|
||||||
|
setCurrentPage(1);
|
||||||
|
} else {
|
||||||
|
setOptionsInternal((prev) => [
|
||||||
|
...(prev || []),
|
||||||
|
...pagingResponse.data,
|
||||||
|
]);
|
||||||
|
setPaging(pagingResponse.paging);
|
||||||
|
setCurrentPage(page);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle simple array response
|
||||||
|
const simpleResponse = response as DefaultOptionType[];
|
||||||
|
setOptionsInternal(simpleResponse);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
} finally {
|
||||||
setLoadingOptions(false);
|
setLoadingOptions(false);
|
||||||
});
|
setHasContentLoading(false);
|
||||||
}, 400),
|
}
|
||||||
[api]
|
}, debounceTimeout),
|
||||||
|
[api, enableInfiniteScroll, debounceTimeout]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelection = useCallback(() => {
|
const handleSelection = useCallback(() => {
|
||||||
setSearchText('');
|
setSearchText('');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onScroll = useCallback(
|
||||||
|
async (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
if (!enableInfiniteScroll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentTarget } = e;
|
||||||
|
if (
|
||||||
|
currentTarget.scrollTop + currentTarget.offsetHeight ===
|
||||||
|
currentTarget.scrollHeight
|
||||||
|
) {
|
||||||
|
const currentOptionsLength = optionsInternal?.length ?? 0;
|
||||||
|
if (currentOptionsLength < paging.total && !hasContentLoading) {
|
||||||
|
await fetchOptions(searchText, currentPage + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
enableInfiniteScroll,
|
||||||
|
optionsInternal,
|
||||||
|
paging.total,
|
||||||
|
hasContentLoading,
|
||||||
|
searchText,
|
||||||
|
currentPage,
|
||||||
|
fetchOptions,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const dropdownRender = useCallback(
|
||||||
|
(menu: React.ReactElement) => (
|
||||||
|
<>
|
||||||
|
{menu}
|
||||||
|
{hasContentLoading && <Loader size="small" />}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[hasContentLoading]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchOptions(searchText);
|
fetchOptions(searchText, 1);
|
||||||
}, [searchText]);
|
}, [searchText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
dropdownRender={enableInfiniteScroll ? dropdownRender : undefined}
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
notFoundContent={loadingOptions ? <Loader size="small" /> : null}
|
notFoundContent={loadingOptions ? <Loader size="small" /> : null}
|
||||||
options={optionsInternal}
|
options={optionsInternal}
|
||||||
searchValue={searchText}
|
searchValue={searchText}
|
||||||
suffixIcon={loadingOptions && <Loader size="small" />} // Controlling the search value to get the initial suggestions when not typed anything
|
suffixIcon={loadingOptions && <Loader size="small" />}
|
||||||
|
onPopupScroll={enableInfiniteScroll ? onScroll : undefined}
|
||||||
onSearch={(value: string) => {
|
onSearch={(value: string) => {
|
||||||
setSearchText(value);
|
setSearchText(value);
|
||||||
setLoadingOptions(true);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
onSelect={handleSelection}
|
onSelect={handleSelection}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { DefaultOptionType } from 'antd/lib/select';
|
||||||
|
import { PagingResponse } from './AsyncSelect';
|
||||||
|
|
||||||
|
export interface AsyncSelectListProps {
|
||||||
|
api: (
|
||||||
|
queryString: string,
|
||||||
|
page?: number
|
||||||
|
) => Promise<DefaultOptionType[] | PagingResponse<DefaultOptionType[]>>;
|
||||||
|
enableInfiniteScroll?: boolean;
|
||||||
|
debounceTimeout?: number;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user