-
Couldn't load subscription status.
- Fork 403
feat(clerk-js,types,localizations): Search members on OrganizationProfile
#4942
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 all commits
75f24a5
45afbc8
dc6c79b
d0ba4d8
3a04b22
a4cfd70
e7320a4
b6ce5c7
ff56867
7e794d7
21d3b31
2738e0d
333cf78
3b5e46c
d6b7fe0
f4ce29e
f2f0a4f
213b0c6
a50ad25
4214390
9f595d1
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 |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| '@clerk/localizations': patch | ||
| '@clerk/clerk-js': patch | ||
| '@clerk/shared': patch | ||
| '@clerk/types': patch | ||
| --- | ||
|
|
||
| Introduced searching for members list on `OrganizationProfile` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import type { useOrganization } from '@clerk/shared/react'; | ||
| import type { GetMembersParams } from '@clerk/types'; | ||
| import { useEffect, useRef } from 'react'; | ||
|
|
||
| import { descriptors, Flex, Icon, localizationKeys, useLocalizations } from '../../../ui/customizables'; | ||
| import { Animated, InputWithIcon } from '../../../ui/elements'; | ||
| import { MagnifyingGlass } from '../../../ui/icons'; | ||
| import { Spinner } from '../../../ui/primitives'; | ||
| import { ACTIVE_MEMBERS_PAGE_SIZE } from './OrganizationMembers'; | ||
|
|
||
| type MembersSearchProps = { | ||
| /** | ||
| * Controlled query param state by parent component | ||
| */ | ||
| query: GetMembersParams['query']; | ||
| /** | ||
| * Controlled input field value by parent component | ||
| */ | ||
| value: string; | ||
| /** | ||
| * Paginated organization memberships | ||
| */ | ||
| memberships: ReturnType<typeof useOrganization>['memberships']; | ||
| /** | ||
| * Handler for change event on input field | ||
| */ | ||
| onSearchChange: (value: string) => void; | ||
| /** | ||
| * Handler for `query` value changes | ||
| */ | ||
| onQueryTrigger: (query: string) => void; | ||
| }; | ||
|
|
||
| const membersSearchDebounceMs = 500; | ||
|
|
||
| export const MembersSearch = ({ query, value, memberships, onSearchChange, onQueryTrigger }: MembersSearchProps) => { | ||
| const { t } = useLocalizations(); | ||
|
|
||
| const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
|
|
||
| const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
| const eventValue = event.target.value; | ||
| onSearchChange(eventValue); | ||
|
|
||
| const shouldClearQuery = eventValue === ''; | ||
| if (shouldClearQuery) { | ||
| onQueryTrigger(eventValue); | ||
| } | ||
| }; | ||
|
|
||
| // Debounce the input value changes until the user stops typing | ||
| // to trigger the `query` param setter | ||
| function handleKeyUp() { | ||
| if (debounceTimer.current) { | ||
| clearTimeout(debounceTimer.current); | ||
| } | ||
|
|
||
| debounceTimer.current = setTimeout(() => { | ||
| onQueryTrigger(value.trim()); | ||
| }, membersSearchDebounceMs); | ||
| } | ||
|
|
||
| // If search is not performed on a initial page, resets pagination offset | ||
| // based on the response count | ||
| useEffect(() => { | ||
| if (!query || !memberships?.data) { | ||
| return; | ||
| } | ||
|
|
||
| const hasOnePageLeft = (memberships?.count ?? 0) <= ACTIVE_MEMBERS_PAGE_SIZE; | ||
| if (hasOnePageLeft) { | ||
| memberships?.fetchPage?.(1); | ||
| } | ||
| }, [query, memberships]); | ||
|
|
||
| const isFetchingNewData = value && !!memberships?.isLoading && !!memberships.data?.length; | ||
|
|
||
| return ( | ||
| <Animated asChild> | ||
| <Flex sx={{ width: '100%' }}> | ||
| <InputWithIcon | ||
| value={value} | ||
| type='search' | ||
| autoCapitalize='none' | ||
| spellCheck={false} | ||
| aria-label='Search' | ||
| placeholder={t(localizationKeys('organizationProfile.membersPage.action__search'))} | ||
| leftIcon={ | ||
| isFetchingNewData ? ( | ||
| <Spinner size='xs' /> | ||
| ) : ( | ||
| <Icon | ||
| icon={MagnifyingGlass} | ||
| elementDescriptor={descriptors.organizationProfileMembersSearchInputIcon} | ||
| /> | ||
| ) | ||
| } | ||
| onKeyUp={handleKeyUp} | ||
| onChange={handleChange} | ||
| elementDescriptor={descriptors.organizationProfileMembersSearchInput} | ||
| /> | ||
| </Flex> | ||
| </Animated> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -518,7 +518,7 @@ describe('OrganizationMembers', () => { | |
| await waitFor(async () => | ||
| expect(await findByRole('heading', { name: /invite new members/i })).toBeInTheDocument(), | ||
| ); | ||
| expect(inviteButton).not.toBeInTheDocument(); | ||
| expect(inviteButton).toBeInTheDocument(); | ||
|
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. |
||
| await userEvent.click(getByRole('button', { name: 'Cancel' })); | ||
|
|
||
| await waitFor(async () => expect(await findByRole('button', { name: 'Invite' })).toBeInTheDocument()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,18 +4,19 @@ import { useActionContext } from './ActionRoot'; | |
|
|
||
| type ActionTriggerProps = PropsWithChildren<{ | ||
| value: string; | ||
| hideOnActive?: boolean; | ||
| }>; | ||
|
|
||
| export const ActionTrigger = (props: ActionTriggerProps) => { | ||
| const { children, value } = props; | ||
| const { children, value, hideOnActive = true } = props; | ||
|
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. This preserves the "invite" action button alongside the search input, otherwise, it'll lead to a layout height jump: CleanShot.2025-01-21.at.08.30.04.mp4 |
||
| const { active, open } = useActionContext(); | ||
|
|
||
| const validChildren = Children.only(children); | ||
| if (!isValidElement(validChildren)) { | ||
| throw new Error('Children of ActionTrigger must be a valid element'); | ||
| } | ||
|
|
||
| if (active === value) { | ||
| if (hideOnActive && active === value) { | ||
| return null; | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.