Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 470
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
75f24a545afbc8dc6c79bd0ba4d83a04b22a4cfd70e7320a4b6ce5c7ff568677e794d721d3b312738e0d333cf783b5e46cd6b7fe0f4ce29ef2f0a4f213b0c6a50ad2542143909f595d1File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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(); | ||
ContributorAuthor 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; | ||
ContributorAuthor 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.
Uh oh!
There was an error while loading. Please reload this page.