Skip to content

Commit

Permalink
feat(UiKit): Channels select (#31918)
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagoevanp authored May 15, 2024
1 parent 2d84fe2 commit ee5cdfc
Show file tree
Hide file tree
Showing 15 changed files with 494 additions and 26 deletions.
6 changes: 6 additions & 0 deletions .changeset/strong-humans-bow.md
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 channels
8 changes: 8 additions & 0 deletions packages/fuselage-ui-kit/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,12 @@ export default {
},
],
},
moduleNameMapper: {
'\\.css$': 'identity-obj-proxy',
'^react($|/.+)': '<rootDir>/../../node_modules/react$1',
},
setupFilesAfterEnv: [
'@testing-library/jest-dom/extend-expect',
'<rootDir>/jest.setup.ts',
],
};
11 changes: 11 additions & 0 deletions packages/fuselage-ui-kit/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { TextEncoder, TextDecoder } from 'util';

global.TextEncoder = TextEncoder;
// @ts-ignore

Check warning on line 4 in packages/fuselage-ui-kit/jest.setup.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Do not use "@ts-ignore" because it alters compilation errors
global.TextDecoder = TextDecoder;

global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
7 changes: 6 additions & 1 deletion packages/fuselage-ui-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
".:build:cjs": "tsc -p tsconfig-cjs.json",
"test": "jest",
"lint": "eslint --ext .js,.jsx,.ts,.tsx .",
"testunit": "jest",
"typecheck": "tsc --noEmit",
"docs": "cross-env NODE_ENV=production build-storybook -o ../../static/fuselage-ui-kit",
"storybook": "start-storybook -p 6006 --no-version-updates",
Expand Down Expand Up @@ -63,11 +64,13 @@
"@babel/preset-react": "~7.22.15",
"@babel/preset-typescript": "~7.22.15",
"@rocket.chat/apps-engine": "^1.42.2",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/eslint-config": "workspace:^",
"@rocket.chat/fuselage": "^0.53.6",
"@rocket.chat/fuselage-hooks": "^0.33.1",
"@rocket.chat/fuselage-polyfills": "~0.31.25",
"@rocket.chat/icons": "^0.35.0",
"@rocket.chat/mock-providers": "workspace:^",
"@rocket.chat/prettier-config": "~0.31.25",
"@rocket.chat/styled": "~0.31.25",
"@rocket.chat/ui-avatar": "workspace:^",
Expand All @@ -82,8 +85,9 @@
"@storybook/source-loader": "~6.5.16",
"@storybook/theming": "~6.5.16",
"@tanstack/react-query": "^4.16.1",
"@testing-library/react": "^14.2.2",
"@testing-library/react": "^12.1.4",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/babel__core": "^7.20.3",
"@types/babel__preset-env": "^7.9.4",
"@types/react": "~17.0.69",
Expand All @@ -106,6 +110,7 @@
"typescript": "~5.3.3"
},
"dependencies": {
"@rocket.chat/core-typings": "*",
"@rocket.chat/gazzodown": "workspace:^",
"@rocket.chat/ui-kit": "workspace:~",
"tslib": "^2.5.3"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
import { MockedServerContext } from '@rocket.chat/mock-providers';
import type { ChannelsSelectElement as ChannelsSelectElementType } 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 ChannelsSelectElement from './ChannelsSelectElement';
import { useChannelsData } from './hooks/useChannelsData';

const channelsBlock: ChannelsSelectElementType = {
type: 'channels_select',
appId: 'test',
blockId: 'test',
actionId: 'test',
};

jest.mock('./hooks/useChannelsData');

const mockedOptions: ReturnType<typeof useChannelsData> = [
{
value: 'channel1_id',
label: {
name: 'Channel 1',
avatarETag: 'test',
type: RoomType.CHANNEL,
},
},
{
value: 'channel2_id',
label: {
name: 'Channel 2',
avatarETag: 'test',
type: RoomType.CHANNEL,
},
},
{
value: 'channel3_id',
label: {
name: 'Channel 3',
avatarETag: 'test',
type: RoomType.CHANNEL,
},
},
];

const mockUseChannelsData = jest.mocked(useChannelsData);
mockUseChannelsData.mockReturnValue(mockedOptions);

describe('UiKit ChannelsSelect Element', () => {
beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
});

beforeEach(() => {
render(
<MockedServerContext>
<ChannelsSelectElement
index={0}
block={channelsBlock}
context={BlockContext.FORM}
surfaceRenderer={contextualBarParser}
/>
</MockedServerContext>
);
});

it('should render a UiKit channel selector', async () => {
expect(await screen.findByRole('textbox')).toBeInTheDocument();
});

it('should open the channel selector', async () => {
const input = await screen.findByRole('textbox');
input.focus();

expect(await screen.findByRole('listbox')).toBeInTheDocument();
});

it('should select a channel', 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('channel1_id');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
AutoComplete,
Option,
Box,
Options,
Chip,
} from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { RoomAvatar } from '@rocket.chat/ui-avatar';
import type * as UiKit from '@rocket.chat/ui-kit';
import { memo, useCallback, useState } from 'react';

import { useUiKitState } from '../../hooks/useUiKitState';
import type { BlockProps } from '../../utils/BlockProps';
import { useChannelsData } from './hooks/useChannelsData';

type ChannelsSelectElementProps = BlockProps<UiKit.ChannelsSelectElement>;

const ChannelsSelectElement = ({
block,
context,
}: ChannelsSelectElementProps) => {
const [{ value, loading }, action] = useUiKitState(block, context);

const [filter, setFilter] = useState('');
const filterDebounced = useDebouncedValue(filter, 300);

const options = useChannelsData({ filter: filterDebounced });

const handleChange = useCallback(
(value) => {
action({ target: { value } });
},
[action]
);

return (
<AutoComplete
value={value}
onChange={handleChange}
disabled={loading}
filter={filter}
setFilter={setFilter}
renderSelected={({ selected: { value, label } }) => (
<Chip height='x20' value={value} mie={4}>
<RoomAvatar
size='x20'
room={{ type: label?.type || 'c', _id: value, ...label }}
/>
<Box verticalAlign='middle' is='span' margin='none' mi={4}>
{label.name}
</Box>
</Chip>
)}
renderItem={({ value, label, ...props }) => (
<Option
key={value}
{...props}
label={label.name}
avatar={
<RoomAvatar
size={Options.AvatarSize}
room={{
type: label.type,
_id: value,
avatarETag: label.avatarETag,
}}
{...props}
/>
}
/>
)}
options={options}
/>
);
};

export default memo(ChannelsSelectElement);
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { MockedServerContext } from '@rocket.chat/mock-providers';
import type { MultiChannelsSelectElement as MultiChannelsSelectElementType } 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 MultiChannelsSelectElement from './MultiChannelsSelectElement';
import { useChannelsData } from './hooks/useChannelsData';

const channelsBlock: MultiChannelsSelectElementType = {
type: 'multi_channels_select',
appId: 'test',
blockId: 'test',
actionId: 'test',
};

jest.mock('./hooks/useChannelsData');

const mockedOptions: ReturnType<typeof useChannelsData> = [
{
value: 'channel1_id',
label: {
name: 'Channel 1',
avatarETag: 'test',
type: 'c',
},
},
{
value: 'channel2_id',
label: {
name: 'Channel 2',
avatarETag: 'test',
type: 'c',
},
},
{
value: 'channel3_id',
label: {
name: 'Channel 3',
avatarETag: 'test',
type: 'c',
},
},
];

const mockUseChannelsData = jest.mocked(useChannelsData);
mockUseChannelsData.mockReturnValue(mockedOptions);

describe('UiKit MultiChannelsSelect Element', () => {
beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
});

beforeEach(() => {
render(
<MockedServerContext>
<MultiChannelsSelectElement
index={0}
block={channelsBlock}
context={BlockContext.FORM}
surfaceRenderer={contextualBarParser}
/>
</MockedServerContext>
);
});

it('should render a UiKit multiple channels selector', async () => {
expect(await screen.findByRole('textbox')).toBeInTheDocument();
});

it('should open the channels selector', async () => {
const input = await screen.findByRole('textbox');
input.focus();

expect(await screen.findByRole('listbox')).toBeInTheDocument();
});

it('should select channels', 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('channel1_id');
expect(selected[1]).toHaveValue('channel3_id');
});

it('should remove a selected channel', 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('channel1_id');
await userEvent.click(selected1, { delay: null });

const remainingSelected = (await screen.findAllByRole('button'))[0];
expect(remainingSelected).toHaveValue('channel3_id');
});
});
Loading

0 comments on commit ee5cdfc

Please sign in to comment.