Skip to content

Commit 197dfbc

Browse files
authored
TT-14699 : [UI] Dashboard cert search partially works (#411)
* Dashboard cert search partially This PR makes a change to extended the `Combobox2` component with **server-side search capability** through a new `onSearch` prop. **New `onSearch` Prop** - Optional callback function that receives the search term as user types - Enables backend/API-based search instead of only client-side filtering **Smart Debouncing (300ms)** - Prevents API spam by debouncing search calls - Uses stable function reference to avoid re-creating debounced function to avoid unlimited API calls * bump version * extend tests
1 parent 7092a0d commit 197dfbc

9 files changed

Lines changed: 354 additions & 8 deletions

File tree

lib/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/tyk-ui.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/tyk-ui.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tyk-technologies/tyk-ui",
3-
"version": "4.4.21",
3+
"version": "4.4.22",
44
"description": "Tyk UI - ui reusable components",
55
"main": "src/index.js",
66
"scripts": {

src/form/components/Combobox2/Combobox2.test.js

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,4 +630,265 @@ describe('Combobox2', () => {
630630
.should('have.text', item.name);
631631
});
632632
});
633+
634+
describe('onSearch prop for server-side search', () => {
635+
it('does not call onSearch on mount', () => {
636+
const onSearchSpy = cy.spy().as('onSearchSpy');
637+
638+
cy.mount(
639+
<Combobox2
640+
values={items}
641+
value=""
642+
onSearch={onSearchSpy}
643+
showSearch
644+
/>,
645+
);
646+
647+
cy.get('@onSearchSpy').should('not.have.been.called');
648+
});
649+
650+
it('calls onSearch when user types in tag mode', () => {
651+
const onSearchSpy = cy.spy().as('onSearchSpy');
652+
653+
cy.mount(
654+
<Combobox2
655+
values={items}
656+
value={[]}
657+
tags
658+
onSearch={onSearchSpy}
659+
/>,
660+
);
661+
662+
cy.get(`.${classes.entryField}`)
663+
.type('test');
664+
665+
cy.wait(350);
666+
667+
cy.get('@onSearchSpy').should('have.been.calledOnce');
668+
cy.get('@onSearchSpy').should('have.been.calledWith', 'test');
669+
});
670+
671+
it('calls onSearch when user types with showSearch enabled', () => {
672+
const onSearchSpy = cy.spy().as('onSearchSpy');
673+
674+
cy.mount(
675+
<Combobox2
676+
values={items}
677+
value=""
678+
onSearch={onSearchSpy}
679+
showSearch
680+
/>,
681+
);
682+
683+
cy.get(`.${classes.trigger}`)
684+
.click();
685+
686+
cy.get(`.${classes.searchField} input`)
687+
.type('search');
688+
689+
cy.get('@onSearchSpy').should('have.been.calledOnce');
690+
cy.get('@onSearchSpy').should('have.been.calledWith', 'search');
691+
});
692+
693+
it('debounces onSearch calls correctly', () => {
694+
const onSearchSpy = cy.spy().as('onSearchSpy');
695+
696+
cy.mount(
697+
<Combobox2
698+
values={items}
699+
value={[]}
700+
tags
701+
onSearch={onSearchSpy}
702+
/>,
703+
);
704+
705+
cy.get(`.${classes.entryField}`)
706+
.type('a');
707+
708+
cy.wait(100);
709+
710+
cy.get(`.${classes.entryField}`)
711+
.type('b');
712+
713+
cy.wait(100);
714+
715+
cy.get(`.${classes.entryField}`)
716+
.type('c');
717+
718+
cy.get('@onSearchSpy').should('not.have.been.called');
719+
720+
cy.wait(350);
721+
722+
cy.get('@onSearchSpy').should('have.been.calledOnce');
723+
cy.get('@onSearchSpy').should('have.been.calledWith', 'abc');
724+
});
725+
726+
it('calls onSearch with empty string when search is cleared after searching', () => {
727+
const onSearchSpy = cy.spy().as('onSearchSpy');
728+
729+
cy.mount(
730+
<Combobox2
731+
values={items}
732+
value={[]}
733+
tags
734+
onSearch={onSearchSpy}
735+
/>,
736+
);
737+
738+
cy.get(`.${classes.entryField}`)
739+
.type('test');
740+
741+
cy.wait(350);
742+
743+
cy.get('@onSearchSpy').should('have.been.calledWith', 'test');
744+
745+
cy.get(`.${classes.entryField}`)
746+
.clear();
747+
748+
cy.wait(350);
749+
750+
cy.get('@onSearchSpy').should('have.been.calledWith', '');
751+
});
752+
753+
it('does not perform client-side filtering when onSearch is provided', () => {
754+
function Comp() {
755+
const [searchResults, setSearchResults] = useState(items);
756+
757+
const handleSearch = (searchTerm) => {
758+
if (!searchTerm) {
759+
setSearchResults(items);
760+
} else {
761+
setSearchResults(items.filter((item) => item.name === searchTerm));
762+
}
763+
};
764+
765+
return (
766+
<Combobox2
767+
values={searchResults}
768+
value={[]}
769+
tags
770+
onSearch={handleSearch}
771+
/>
772+
);
773+
}
774+
775+
cy.mount(<Comp />);
776+
777+
cy.get(`.${classes.entryField}`)
778+
.type('Item2');
779+
780+
cy.wait(350);
781+
782+
cy.get(`.${classes.dropdownList} li`)
783+
.should('have.length', 1)
784+
.and('contain.text', 'Item2');
785+
});
786+
787+
it('works correctly with multiple value updates', () => {
788+
function Comp() {
789+
const [searchResults, setSearchResults] = useState(items);
790+
791+
const handleSearch = (searchTerm) => {
792+
if (!searchTerm) {
793+
setSearchResults(items);
794+
} else {
795+
setSearchResults(
796+
items.filter((item) => item.name.toLowerCase().includes(searchTerm.toLowerCase())),
797+
);
798+
}
799+
};
800+
801+
return (
802+
<Combobox2
803+
values={searchResults}
804+
value=""
805+
onSearch={handleSearch}
806+
showSearch
807+
/>
808+
);
809+
}
810+
811+
cy.mount(<Comp />);
812+
813+
cy.get(`.${classes.trigger}`)
814+
.click();
815+
816+
cy.get(`.${classes.dropdownList} li`)
817+
.should('have.length', items.length);
818+
819+
cy.get(`.${classes.searchField} input`)
820+
.type('item2');
821+
822+
cy.wait(350);
823+
824+
cy.get(`.${classes.dropdownList} li`)
825+
.should('have.length', 1)
826+
.and('contain.text', 'Item2');
827+
828+
cy.get(`.${classes.searchField} input`)
829+
.clear();
830+
831+
cy.wait(350);
832+
833+
cy.get(`.${classes.dropdownList} li`)
834+
.should('have.length', items.length);
835+
});
836+
837+
it('maintains backward compatibility - works without onSearch prop', () => {
838+
cy.mount(
839+
<Combobox2
840+
values={items}
841+
value=""
842+
showSearch
843+
/>,
844+
);
845+
846+
cy.get(`.${classes.trigger}`)
847+
.click();
848+
849+
cy.get(`.${classes.dropdownList} li`)
850+
.should('have.length', items.length);
851+
852+
cy.get(`.${classes.searchField} input`)
853+
.type('Item2');
854+
855+
cy.get(`.${classes.dropdownList} li`)
856+
.should('have.length', 1)
857+
.and('contain.text', 'Item2');
858+
});
859+
860+
it('handles rapid typing and clearing correctly', () => {
861+
const onSearchSpy = cy.spy().as('onSearchSpy');
862+
863+
cy.mount(
864+
<Combobox2
865+
values={items}
866+
value={[]}
867+
tags
868+
onSearch={onSearchSpy}
869+
/>,
870+
);
871+
872+
cy.get(`.${classes.entryField}`)
873+
.type('test');
874+
875+
cy.wait(350);
876+
877+
cy.get('@onSearchSpy').should('have.been.calledWith', 'test');
878+
879+
cy.get(`.${classes.entryField}`)
880+
.clear();
881+
882+
cy.wait(350);
883+
884+
cy.get('@onSearchSpy').should('have.been.calledWith', '');
885+
886+
cy.get(`.${classes.entryField}`)
887+
.type('new');
888+
889+
cy.wait(350);
890+
891+
cy.get('@onSearchSpy').should('have.been.calledWith', 'new');
892+
});
893+
});
633894
});

src/form/components/Combobox2/Readme.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,4 +626,54 @@ import Icon from '../../../components/Icon';
626626
placeholder="Please select a value"
627627
validateOnChange={(values, lastValue) => {console.log(lastValue, !isNaN(lastValue.id)); return !isNaN(lastValue.id) ? undefined : 'Added value is not a number'}}
628628
/>
629-
```
629+
```
630+
631+
#### Combobox with dynamic server side search using `onSearch`
632+
633+
```js
634+
import { useState, useEffect } from 'react';
635+
636+
const [posts, setPosts] = useState([]);
637+
const [loading, setLoading] = useState(false);
638+
const [selectedPost, setSelectedPost] = useState(null);
639+
640+
// Load initial posts on mount
641+
useEffect(() => {
642+
fetchPosts('');
643+
}, []);
644+
645+
async function fetchPosts(searchTerm) {
646+
setLoading(true);
647+
try {
648+
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${searchTerm}`);
649+
const data = await response.json();
650+
651+
setPosts(data.map(post => ({
652+
id: post.id,
653+
name: post.title,
654+
body: post.body
655+
})));
656+
} catch (error) {
657+
console.error('Error fetching posts:', error);
658+
} finally {
659+
setLoading(false);
660+
}
661+
}
662+
663+
<Combobox2
664+
values={posts.length > 0 ? posts : [
665+
{id: 201, name: '201'},
666+
{id: 'aaa', name: 'AAA'}
667+
]}
668+
value={selectedPost}
669+
label="Search Posts (Backend Search)"
670+
placeholder="Type to search posts..."
671+
onChange={(selected) => {
672+
console.log('Selected:', selected);
673+
setSelectedPost(selected);
674+
}}
675+
onSearch={fetchPosts}
676+
note={loading ? 'Loading...' : 'Search results will be fetched from the backend'}
677+
showSearch
678+
/>
679+
```

0 commit comments

Comments
 (0)