-
Notifications
You must be signed in to change notification settings - Fork 329
Enhancement/11889 server pagination #12242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
9712417
1297ddf
21c444a
7e561be
c569bc0
77f435b
589e420
a815589
c3ace60
8e1cb84
305554e
47f80cc
97adf22
4c4487e
9ddd1e5
c6e5b27
eb43f42
f0b95e9
210efed
a4e31cf
5ee6023
e1622ce
9049677
16b7bab
7a87ef7
a915c6e
79fd70a
51e9547
5da33fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,36 +25,22 @@ import PropTypes from 'prop-types'; | |
| * WordPress dependencies | ||
| */ | ||
| import { __ } from '@wordpress/i18n'; | ||
| import { useCallback, useState, useEffect } from '@wordpress/element'; | ||
| import { useCallback } from '@wordpress/element'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { useDebounce } from '@/js/hooks/useDebounce'; | ||
| import CloseIcon from '@/svg/icons/close.svg'; | ||
|
|
||
| export default function InviteSearchInput( { show = true, value, onChange } ) { | ||
| const [ inputValue, setInputValue ] = useState( value ); | ||
| const debouncedOnChange = useDebounce( onChange, 300 ); | ||
|
|
||
| // Sync external value changes (e.g., when panel resets). | ||
| useEffect( () => { | ||
| setInputValue( value ); | ||
| }, [ value ] ); | ||
|
Comment on lines
-44
to
-46
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No longer needed, because the component is fully controlled (value comes directly from parent, no local inputValue state). |
||
|
|
||
| const handleChange = useCallback( | ||
| ( event ) => { | ||
| setInputValue( event.target.value ); | ||
| debouncedOnChange( event.target.value ); | ||
| }, | ||
| [ debouncedOnChange ] | ||
| ( event ) => onChange( event.target.value ), | ||
| [ onChange ] | ||
| ); | ||
|
|
||
| const handleClear = useCallback( () => { | ||
| setInputValue( '' ); | ||
| debouncedOnChange.cancel(); | ||
| onChange( '' ); | ||
| }, [ debouncedOnChange, onChange ] ); | ||
| }, [ onChange ] ); | ||
|
|
||
| if ( ! show ) { | ||
| return null; | ||
|
|
@@ -73,10 +59,10 @@ export default function InviteSearchInput( { show = true, value, onChange } ) { | |
| 'Search user name, role or email', | ||
| 'google-site-kit' | ||
| ) } | ||
| value={ inputValue } | ||
| value={ value } | ||
| onChange={ handleChange } | ||
| /> | ||
| { inputValue && ( | ||
| { value && ( | ||
| <span | ||
| className="googlesitekit-invite-search-input__clear" | ||
| onClick={ handleClear } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| /** | ||
| * InviteSearchInput tests. | ||
| * | ||
| * Site Kit by Google, Copyright 2026 Google LLC | ||
| * | ||
| * 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 | ||
| * | ||
| * https://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. | ||
| */ | ||
|
|
||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { fireEvent } from '@testing-library/react'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { createTestRegistry, render } from '../../../../../tests/js/test-utils'; | ||
| import InviteSearchInput from './InviteSearchInput'; | ||
|
|
||
| describe( 'InviteSearchInput', () => { | ||
| let registry; | ||
|
|
||
| beforeEach( () => { | ||
| registry = createTestRegistry(); | ||
| } ); | ||
|
|
||
| it( 'calls onChange on every keystroke', () => { | ||
| const onChange = jest.fn(); | ||
| const { getByLabelText } = render( | ||
| <InviteSearchInput value="" onChange={ onChange } />, | ||
| { registry } | ||
| ); | ||
|
|
||
| fireEvent.change( getByLabelText( 'Search user name, role or email' ), { | ||
| target: { value: 'a' }, | ||
| } ); | ||
| fireEvent.change( getByLabelText( 'Search user name, role or email' ), { | ||
| target: { value: 'ab' }, | ||
| } ); | ||
|
|
||
| expect( onChange ).toHaveBeenNthCalledWith( 1, 'a' ); | ||
| expect( onChange ).toHaveBeenNthCalledWith( 2, 'ab' ); | ||
| } ); | ||
|
|
||
| it( 'clears search when clear button is clicked', () => { | ||
| const onChange = jest.fn(); | ||
| const { getByLabelText } = render( | ||
| <InviteSearchInput value="abc" onChange={ onChange } />, | ||
| { registry } | ||
| ); | ||
|
|
||
| fireEvent.click( getByLabelText( 'Clear search' ) ); | ||
|
|
||
| expect( onChange ).toHaveBeenCalledWith( '' ); | ||
| } ); | ||
| } ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,6 @@ import PropTypes from 'prop-types'; | |
| * WordPress dependencies | ||
| */ | ||
| import { __ } from '@wordpress/i18n'; | ||
| import { useMemo } from '@wordpress/element'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
|
|
@@ -41,33 +40,10 @@ function EmptyMessage( { text } ) { | |
|
|
||
| export default function InviteUserList( { | ||
| users, | ||
| searchTerm, | ||
| inviteResults, | ||
| onInviteResult, | ||
| isLoading, | ||
| } ) { | ||
| const filteredUsers = useMemo( () => { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filtering is handled server side now |
||
| if ( ! searchTerm ) { | ||
| return users; | ||
| } | ||
|
|
||
| const lowerSearchTerm = searchTerm.toLowerCase(); | ||
|
|
||
| return users.filter( ( user ) => { | ||
| const nameMatch = user.name | ||
| ?.toLowerCase() | ||
| .includes( lowerSearchTerm ); | ||
| const emailMatch = user.email | ||
| ?.toLowerCase() | ||
| .includes( lowerSearchTerm ); | ||
| const roleMatch = user.role | ||
| ?.toLowerCase() | ||
| .includes( lowerSearchTerm ); | ||
|
|
||
| return nameMatch || emailMatch || roleMatch; | ||
| } ); | ||
| }, [ users, searchTerm ] ); | ||
|
|
||
| if ( isLoading ) { | ||
| return <InviteUserSkeletonList visibleItems={ 3 } />; | ||
| } | ||
|
|
@@ -83,17 +59,9 @@ export default function InviteUserList( { | |
| ); | ||
| } | ||
|
|
||
| if ( filteredUsers.length === 0 ) { | ||
| return ( | ||
| <EmptyMessage | ||
| text={ __( 'No users match your search.', 'google-site-kit' ) } | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="googlesitekit-invite-user-list"> | ||
| { filteredUsers.map( ( user ) => ( | ||
| { users.map( ( user ) => ( | ||
| <InviteUserRow | ||
| key={ user.id } | ||
| user={ user } | ||
|
|
@@ -114,7 +82,6 @@ InviteUserList.propTypes = { | |
| role: PropTypes.string, | ||
| } ) | ||
| ).isRequired, | ||
| searchTerm: PropTypes.string, | ||
| inviteResults: PropTypes.objectOf( | ||
| PropTypes.shape( { | ||
| status: PropTypes.oneOf( [ 'success', 'error' ] ), | ||
|
|
@@ -126,7 +93,6 @@ InviteUserList.propTypes = { | |
| }; | ||
|
|
||
| InviteUserList.defaultProps = { | ||
| searchTerm: '', | ||
| inviteResults: {}, | ||
| isLoading: false, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,80 +66,6 @@ describe( 'InviteUserList', () => { | |
| expect( getByText( 'AuthorUser' ) ).toBeInTheDocument(); | ||
| } ); | ||
|
|
||
| it( 'filters users by name match', () => { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filter related tests are redundant now, as filtering is handled server side |
||
| const { getByText, queryByText } = render( | ||
| <InviteUserList | ||
| users={ mockUsers } | ||
| searchTerm="MainAdmin" | ||
| onInviteResult={ mockOnInviteResult } | ||
| />, | ||
| { registry } | ||
| ); | ||
|
|
||
| expect( getByText( 'MainAdminName' ) ).toBeInTheDocument(); | ||
| expect( queryByText( 'EditorUser' ) ).not.toBeInTheDocument(); | ||
| expect( queryByText( 'AuthorUser' ) ).not.toBeInTheDocument(); | ||
| } ); | ||
|
|
||
| it( 'filters users by email match', () => { | ||
| const { getByText, queryByText } = render( | ||
| <InviteUserList | ||
| users={ mockUsers } | ||
| searchTerm="editor@example" | ||
| onInviteResult={ mockOnInviteResult } | ||
| />, | ||
| { registry } | ||
| ); | ||
|
|
||
| expect( getByText( 'EditorUser' ) ).toBeInTheDocument(); | ||
| expect( queryByText( 'MainAdminName' ) ).not.toBeInTheDocument(); | ||
| expect( queryByText( 'AuthorUser' ) ).not.toBeInTheDocument(); | ||
| } ); | ||
|
|
||
| it( 'filters users by role match', () => { | ||
| const { getByText, queryByText } = render( | ||
| <InviteUserList | ||
| users={ mockUsers } | ||
| searchTerm="author" | ||
| onInviteResult={ mockOnInviteResult } | ||
| />, | ||
| { registry } | ||
| ); | ||
|
|
||
| expect( getByText( 'AuthorUser' ) ).toBeInTheDocument(); | ||
| expect( queryByText( 'MainAdminName' ) ).not.toBeInTheDocument(); | ||
| expect( queryByText( 'EditorUser' ) ).not.toBeInTheDocument(); | ||
| } ); | ||
|
|
||
| it( 'performs case-insensitive search', () => { | ||
| const { getByText, queryByText } = render( | ||
| <InviteUserList | ||
| users={ mockUsers } | ||
| searchTerm="MAINADMIN" | ||
| onInviteResult={ mockOnInviteResult } | ||
| />, | ||
| { registry } | ||
| ); | ||
|
|
||
| expect( getByText( 'MainAdminName' ) ).toBeInTheDocument(); | ||
| expect( queryByText( 'EditorUser' ) ).not.toBeInTheDocument(); | ||
| } ); | ||
|
|
||
| it( 'shows "No users match your search" when filter returns empty', () => { | ||
| const { getByText } = render( | ||
| <InviteUserList | ||
| users={ mockUsers } | ||
| searchTerm="nonexistentuser" | ||
| onInviteResult={ mockOnInviteResult } | ||
| />, | ||
| { registry } | ||
| ); | ||
|
|
||
| expect( | ||
| getByText( 'No users match your search.' ) | ||
| ).toBeInTheDocument(); | ||
| } ); | ||
|
|
||
| it( 'shows empty state when users array is empty', () => { | ||
| const { getByText } = render( | ||
| <InviteUserList | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debounce and usage are not needed here, as it is handled in parent component