Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9712417
Remove local debounce from invite search input.
zutigrm Feb 26, 2026
1297ddf
Cover direct onChange behavior in invite search input.
zutigrm Feb 26, 2026
21c444a
Remove client-side filtering from invite user list.
zutigrm Feb 26, 2026
7e561be
Update invite user list assertions for server-filtered users.
zutigrm Feb 26, 2026
c569bc0
Use debounced server search and avoid duplicate eligible-subscriber f…
zutigrm Feb 26, 2026
77f435b
Align invite stories with paginated/searchable subscriber response.
zutigrm Feb 26, 2026
589e420
Update invite panel tests for debounced server search and reset behav…
zutigrm Feb 26, 2026
a815589
Update stories.
zutigrm Feb 26, 2026
c3ace60
Cache eligible subscribers by query and fetch all pages for search re…
zutigrm Feb 26, 2026
8e1cb84
Add query-key caching and paginated resolver coverage for eligible su…
zutigrm Feb 26, 2026
305554e
Add searchable/paginated eligible subscriber query with merged-id pag…
zutigrm Feb 26, 2026
47f80cc
Return paginated eligible subscribers payload with total metadata.
zutigrm Feb 26, 2026
97adf22
Cover eligible subscriber search, pagination, count, and deduplication.
zutigrm Feb 26, 2026
4c4487e
Verify eligible subscriber REST pagination/search response shape and …
zutigrm Feb 26, 2026
9ddd1e5
Make eligible user request only when panel is opened.
zutigrm Feb 26, 2026
c6e5b27
Fix merge conflicts.
zutigrm Mar 2, 2026
eb43f42
Refactor invite search input .
zutigrm Mar 2, 2026
f0b95e9
Update invite search input tests for direct onChange behavior.
zutigrm Mar 2, 2026
210efed
Remove client filtering; show search-specific empty state from server…
zutigrm Mar 2, 2026
a4e31cf
Add coverage for search empty-state message.
zutigrm Mar 2, 2026
5ee6023
Reset invite panel state on open and pass search term to list.
zutigrm Mar 2, 2026
e1622ce
Align invite panel tests with server-driven search/reset flow.
zutigrm Mar 2, 2026
9049677
Support param-keyed eligible-subscriber cache.
zutigrm Mar 2, 2026
16b7bab
Update email reporting tests.
zutigrm Mar 2, 2026
7a87ef7
Remove query cap.
zutigrm Mar 2, 2026
a915c6e
Add inline comments.
zutigrm Mar 2, 2026
79fd70a
Udate per CR feedback.
zutigrm Mar 4, 2026
51e9547
Add missing test.
zutigrm Mar 4, 2026
5da33fd
Fix merge conflicts.
zutigrm Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,25 @@ import PropTypes from 'prop-types';
*/
import { useInstanceId } from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import { useCallback, useState, useEffect } from '@wordpress/element';
import { useCallback } from '@wordpress/element';

/**
* Internal dependencies
*/
import { useDebounce } from '@/js/hooks/useDebounce';
Copy link
Collaborator Author

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

import VisuallyHidden from '@/js/components/VisuallyHidden';
import CloseIcon from '@/svg/icons/close.svg';

export default function InviteSearchInput( { value = '', onChange } ) {
const instanceID = useInstanceId( InviteSearchInput, 'InviteSearchInput' );
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 ] );

return (
<div className="googlesitekit-invite-search-input">
Expand All @@ -77,10 +64,10 @@ export default function InviteSearchInput( { 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 }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* 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 }
);
const searchInput = getByLabelText(
/Search user name, role, or email/i
);

fireEvent.change( searchInput, {
target: { value: 'a' },
} );
fireEvent.change( searchInput, {
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
Expand Up @@ -25,7 +25,6 @@ import PropTypes from 'prop-types';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { useMemo } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -46,33 +45,22 @@ export default function InviteUserList( {
onInviteResult,
isLoading = false,
} ) {
const filteredUsers = useMemo( () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 } />;
}

if ( users.length === 0 ) {
if ( searchTerm ) {
return (
<EmptyMessage
text={ __(
'No users match your search.',
'google-site-kit'
) }
/>
);
}

return (
<EmptyMessage
text={ __(
Expand All @@ -83,17 +71,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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,91 +66,32 @@ describe( 'InviteUserList', () => {
expect( getByText( 'AuthorUser' ) ).toBeInTheDocument();
} );

it( 'filters users by name match', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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', () => {
it( 'shows empty state when users array is empty', () => {
const { getByText } = render(
<InviteUserList
users={ mockUsers }
searchTerm="nonexistentuser"
users={ [] }
onInviteResult={ mockOnInviteResult }
/>,
{ registry }
);

expect(
getByText( 'No users match your search.' )
getByText( 'No users are eligible to receive invitations.' )
).toBeInTheDocument();
} );

it( 'shows empty state when users array is empty', () => {
it( 'shows no search matches empty state when search term is present', () => {
const { getByText } = render(
<InviteUserList
users={ [] }
searchTerm="john"
onInviteResult={ mockOnInviteResult }
/>,
{ registry }
);

expect(
getByText( 'No users are eligible to receive invitations.' )
getByText( 'No users match your search.' )
).toBeInTheDocument();
} );

Expand Down
Loading
Loading