-
Notifications
You must be signed in to change notification settings - Fork 10.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
526cbf1
commit a565999
Showing
11 changed files
with
419 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@rocket.chat/fuselage-ui-kit": minor | ||
"@rocket.chat/ui-kit": minor | ||
--- | ||
|
||
Introduced new elements for apps to select users |
105 changes: 105 additions & 0 deletions
105
packages/fuselage-ui-kit/src/elements/UsersSelectElement/MultiUsersSelectElement.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { MockedServerContext } from '@rocket.chat/mock-providers'; | ||
import type { MultiUsersSelectElement as MultiUsersSelectElementType } from '@rocket.chat/ui-kit'; | ||
import { BlockContext } from '@rocket.chat/ui-kit'; | ||
import { render, screen } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
|
||
import { contextualBarParser } from '../../surfaces'; | ||
import MultiUsersSelectElement from './MultiUsersSelectElement'; | ||
import { useUsersData } from './hooks/useUsersData'; | ||
|
||
const usersBlock: MultiUsersSelectElementType = { | ||
type: 'multi_users_select', | ||
appId: 'test', | ||
blockId: 'test', | ||
actionId: 'test', | ||
}; | ||
|
||
jest.mock('./hooks/useUsersData'); | ||
|
||
const mockedOptions = [ | ||
{ | ||
value: 'user1_id', | ||
label: 'User 1', | ||
}, | ||
{ | ||
value: 'user2_id', | ||
label: 'User 2', | ||
}, | ||
{ | ||
value: 'user3_id', | ||
label: 'User 3', | ||
}, | ||
]; | ||
|
||
const mockUseUsersData = jest.mocked(useUsersData); | ||
mockUseUsersData.mockReturnValue(mockedOptions); | ||
|
||
describe('UiKit MultiUsersSelect Element', () => { | ||
beforeAll(() => { | ||
jest.useFakeTimers(); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.useRealTimers(); | ||
}); | ||
|
||
beforeEach(() => { | ||
render( | ||
<MockedServerContext> | ||
<MultiUsersSelectElement | ||
index={0} | ||
block={usersBlock} | ||
context={BlockContext.FORM} | ||
surfaceRenderer={contextualBarParser} | ||
/> | ||
</MockedServerContext> | ||
); | ||
}); | ||
|
||
it('should render a UiKit multiple users selector', async () => { | ||
expect(await screen.findByRole('textbox')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should open the users selector', async () => { | ||
const input = await screen.findByRole('textbox'); | ||
input.focus(); | ||
|
||
expect(await screen.findByRole('listbox')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should select users', async () => { | ||
const input = await screen.findByRole('textbox'); | ||
|
||
input.focus(); | ||
|
||
const option1 = (await screen.findAllByRole('option'))[0]; | ||
await userEvent.click(option1, { delay: null }); | ||
|
||
const option2 = (await screen.findAllByRole('option'))[2]; | ||
await userEvent.click(option2, { delay: null }); | ||
|
||
const selected = await screen.findAllByRole('button'); | ||
expect(selected[0]).toHaveValue('user1_id'); | ||
expect(selected[1]).toHaveValue('user3_id'); | ||
}); | ||
|
||
it('should remove a user', async () => { | ||
const input = await screen.findByRole('textbox'); | ||
|
||
input.focus(); | ||
|
||
const option1 = (await screen.findAllByRole('option'))[0]; | ||
await userEvent.click(option1, { delay: null }); | ||
|
||
const option2 = (await screen.findAllByRole('option'))[2]; | ||
await userEvent.click(option2, { delay: null }); | ||
|
||
const selected1 = (await screen.findAllByRole('button'))[0]; | ||
expect(selected1).toHaveValue('user1_id'); | ||
await userEvent.click(selected1, { delay: null }); | ||
|
||
const remainingSelected = (await screen.findAllByRole('button'))[0]; | ||
expect(remainingSelected).toHaveValue('user3_id'); | ||
}); | ||
}); |
76 changes: 76 additions & 0 deletions
76
packages/fuselage-ui-kit/src/elements/UsersSelectElement/MultiUsersSelectElement.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { | ||
Box, | ||
Chip, | ||
AutoComplete, | ||
Option, | ||
OptionAvatar, | ||
OptionContent, | ||
OptionDescription, | ||
} from '@rocket.chat/fuselage'; | ||
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; | ||
import { UserAvatar } from '@rocket.chat/ui-avatar'; | ||
import type * as UiKit from '@rocket.chat/ui-kit'; | ||
import type { ReactElement } from 'react'; | ||
import { memo, useCallback, useState } from 'react'; | ||
|
||
import { useUiKitState } from '../../hooks/useUiKitState'; | ||
import type { BlockProps } from '../../utils/BlockProps'; | ||
import { useUsersData } from './hooks/useUsersData'; | ||
|
||
type MultiUsersSelectElementProps = BlockProps<UiKit.MultiUsersSelectElement>; | ||
|
||
const MultiUsersSelectElement = ({ | ||
block, | ||
context, | ||
}: MultiUsersSelectElementProps): ReactElement => { | ||
const [{ loading, value }, action] = useUiKitState(block, context); | ||
const [filter, setFilter] = useState(''); | ||
|
||
const debouncedFilter = useDebouncedValue(filter, 500); | ||
|
||
const data = useUsersData({ filter: debouncedFilter }); | ||
|
||
const handleChange = useCallback( | ||
(value) => { | ||
action({ target: { value } }); | ||
}, | ||
[action] | ||
); | ||
|
||
return ( | ||
<AutoComplete | ||
value={value || []} | ||
options={data} | ||
placeholder={block.placeholder?.text} | ||
disabled={loading} | ||
filter={filter} | ||
setFilter={setFilter} | ||
onChange={handleChange} | ||
multiple | ||
renderSelected={({ | ||
selected: { value, label }, | ||
onRemove, | ||
...props | ||
}): ReactElement => ( | ||
<Chip {...props} height='x20' value={value} onClick={onRemove} mie={4}> | ||
<UserAvatar size='x20' username={value} /> | ||
<Box is='span' margin='none' mis={4}> | ||
{label} | ||
</Box> | ||
</Chip> | ||
)} | ||
renderItem={({ value, label, ...props }): ReactElement => ( | ||
<Option key={value} {...props}> | ||
<OptionAvatar> | ||
<UserAvatar username={value} size='x20' /> | ||
</OptionAvatar> | ||
<OptionContent> | ||
{label} <OptionDescription>({value})</OptionDescription> | ||
</OptionContent> | ||
</Option> | ||
)} | ||
/> | ||
); | ||
}; | ||
|
||
export default memo(MultiUsersSelectElement); |
82 changes: 82 additions & 0 deletions
82
packages/fuselage-ui-kit/src/elements/UsersSelectElement/UserSelectElement.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { MockedServerContext } from '@rocket.chat/mock-providers'; | ||
import type { UsersSelectElement as UsersSelectElementType } from '@rocket.chat/ui-kit'; | ||
import { BlockContext } from '@rocket.chat/ui-kit'; | ||
import { render, screen } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
|
||
import { contextualBarParser } from '../../surfaces'; | ||
import UsersSelectElement from './UsersSelectElement'; | ||
import { useUsersData } from './hooks/useUsersData'; | ||
|
||
const userBlock: UsersSelectElementType = { | ||
type: 'users_select', | ||
appId: 'test', | ||
blockId: 'test', | ||
actionId: 'test', | ||
}; | ||
|
||
jest.mock('./hooks/useUsersData'); | ||
|
||
const mockedOptions = [ | ||
{ | ||
value: 'user1_id', | ||
label: 'User 1', | ||
}, | ||
{ | ||
value: 'user2_id', | ||
label: 'User 2', | ||
}, | ||
{ | ||
value: 'user3_id', | ||
label: 'User 3', | ||
}, | ||
]; | ||
|
||
const mockUseUsersData = jest.mocked(useUsersData); | ||
mockUseUsersData.mockReturnValue(mockedOptions); | ||
|
||
describe('UiKit UserSelect Element', () => { | ||
beforeAll(() => { | ||
jest.useFakeTimers(); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.useRealTimers(); | ||
}); | ||
|
||
beforeEach(() => { | ||
render( | ||
<MockedServerContext> | ||
<UsersSelectElement | ||
index={0} | ||
block={userBlock} | ||
context={BlockContext.FORM} | ||
surfaceRenderer={contextualBarParser} | ||
/> | ||
</MockedServerContext> | ||
); | ||
}); | ||
|
||
it('should render a UiKit user selector', async () => { | ||
expect(await screen.findByRole('textbox')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should open the user selector', async () => { | ||
const input = await screen.findByRole('textbox'); | ||
input.focus(); | ||
|
||
expect(await screen.findByRole('listbox')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should select a user', async () => { | ||
const input = await screen.findByRole('textbox'); | ||
|
||
input.focus(); | ||
|
||
const option = (await screen.findAllByRole('option'))[0]; | ||
await userEvent.click(option, { delay: null }); | ||
|
||
const selected = await screen.findByRole('button'); | ||
expect(selected).toHaveValue('user1_id'); | ||
}); | ||
}); |
62 changes: 62 additions & 0 deletions
62
packages/fuselage-ui-kit/src/elements/UsersSelectElement/UsersSelectElement.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { AutoComplete, Box, Chip, Option } from '@rocket.chat/fuselage'; | ||
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; | ||
import { UserAvatar } from '@rocket.chat/ui-avatar'; | ||
import type * as UiKit from '@rocket.chat/ui-kit'; | ||
import { useCallback, useState } from 'react'; | ||
|
||
import { useUiKitState } from '../../hooks/useUiKitState'; | ||
import type { BlockProps } from '../../utils/BlockProps'; | ||
import { useUsersData } from './hooks/useUsersData'; | ||
|
||
type UsersSelectElementProps = BlockProps<UiKit.UsersSelectElement>; | ||
|
||
export type UserAutoCompleteOptionType = { | ||
value: string; | ||
label: string; | ||
}; | ||
|
||
const UsersSelectElement = ({ block, context }: UsersSelectElementProps) => { | ||
const [{ value, loading }, action] = useUiKitState(block, context); | ||
|
||
const [filter, setFilter] = useState(''); | ||
const debouncedFilter = useDebouncedValue(filter, 300); | ||
|
||
const data = useUsersData({ filter: debouncedFilter }); | ||
|
||
const handleChange = useCallback( | ||
(value) => { | ||
action({ target: { value } }); | ||
}, | ||
[action] | ||
); | ||
|
||
return ( | ||
<AutoComplete | ||
value={value} | ||
placeholder={block.placeholder?.text} | ||
disabled={loading} | ||
options={data} | ||
onChange={handleChange} | ||
filter={filter} | ||
setFilter={setFilter} | ||
renderSelected={({ selected: { value, label } }) => ( | ||
<Chip height='x20' value={value} mie={4}> | ||
<UserAvatar size='x20' username={value} /> | ||
<Box verticalAlign='middle' is='span' margin='none' mi={4}> | ||
{label} | ||
</Box> | ||
</Chip> | ||
)} | ||
renderItem={({ value, label, ...props }) => ( | ||
<Option | ||
key={value} | ||
{...props} | ||
label={label} | ||
avatar={<UserAvatar username={value} size='x20' />} | ||
/> | ||
)} | ||
/> | ||
); | ||
}; | ||
|
||
export default UsersSelectElement; |
32 changes: 32 additions & 0 deletions
32
packages/fuselage-ui-kit/src/elements/UsersSelectElement/hooks/useUsersData.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { useEndpoint } from '@rocket.chat/ui-contexts'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
|
||
import type { UserAutoCompleteOptionType } from '../UsersSelectElement'; | ||
|
||
type useUsersDataProps = { | ||
filter: string; | ||
}; | ||
|
||
export const useUsersData = ({ filter }: useUsersDataProps) => { | ||
const getUsers = useEndpoint('GET', '/v1/users.autocomplete'); | ||
|
||
const { data } = useQuery( | ||
['users.autoComplete', filter], | ||
async () => { | ||
const users = await getUsers({ | ||
selector: JSON.stringify({ term: filter }), | ||
}); | ||
const options = users.items.map( | ||
(item): UserAutoCompleteOptionType => ({ | ||
value: item.username, | ||
label: item.name || item.username, | ||
}) | ||
); | ||
|
||
return options || []; | ||
}, | ||
{ keepPreviousData: true } | ||
); | ||
|
||
return data; | ||
}; |
Oops, something went wrong.