snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import { Box } from '@rocket.chat/fuselage';
import type { StoryObj, Meta } from '@storybook/react';
import CannedResponse from './CannedResponse';
export default {
component: CannedResponse,
parameters: {
actions: {
argTypesRegex: '^on.*',
},
},
} satisfies Meta<typeof CannedResponse>;
export const Default: StoryObj<typeof CannedResponse> = {
name: 'CannedResponse',
args: {
allowEdit: true,
data: {
shortcut: 'test3 long long long long long long long long long',
text: 'simple canned response test3 long long long long long long long long long long long long long long long longlong long long long long long longlong long long long long long longlong long long long long long longlong long long long long long longlong long long long long long longlong long long long long long long',
scope: 'Customer Support long long long long long long long long long long',
tags: ['sales', 'support', 'long', 'long', 'long', 'long', 'long', 'long', 'long', 'long'],
departmentName: '',
},
},
decorators: [
(fn) => (
<Box height='600px' width='330px'>
{fn()}
</Box>
),
],
};

View File

@@ -0,0 +1,111 @@
import type { ILivechatDepartment, IOmnichannelCannedResponse } from '@rocket.chat/core-typings';
import { Box, Button, ButtonGroup, Tag } from '@rocket.chat/fuselage';
import {
ContextualbarHeader,
ContextualbarTitle,
ContextualbarAction,
ContextualbarContent,
ContextualbarFooter,
ContextualbarDialog,
ContextualbarClose,
} from '@rocket.chat/ui-client';
import type { MouseEventHandler } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useScopeDict } from '../../../hooks/useScopeDict';
export type CannedResponseProps = {
allowEdit: boolean;
allowUse: boolean;
data: {
departmentName?: ILivechatDepartment['name'];
shortcut: IOmnichannelCannedResponse['shortcut'];
text: IOmnichannelCannedResponse['text'];
scope: IOmnichannelCannedResponse['scope'];
tags: IOmnichannelCannedResponse['tags'];
};
onClickBack: MouseEventHandler<HTMLOrSVGElement>;
onClickEdit: MouseEventHandler<HTMLOrSVGElement>;
onClickUse: MouseEventHandler<HTMLOrSVGElement>;
onClose: () => void;
};
const CannedResponse = ({
allowEdit,
allowUse,
data: { departmentName, shortcut, text, scope: dataScope, tags },
onClickBack,
onClickEdit,
onClickUse,
onClose,
}: CannedResponseProps) => {
const { t } = useTranslation();
const scope = useScopeDict(dataScope, departmentName);
return (
<ContextualbarDialog>
<ContextualbarHeader>
{onClickBack && <ContextualbarAction onClick={onClickBack} title={t('Back_to_canned_responses')} name='arrow-back' />}
<ContextualbarTitle>!{shortcut}</ContextualbarTitle>
{onClose && <ContextualbarClose onClick={onClose} />}
</ContextualbarHeader>
<ContextualbarContent>
<Box paddingBlock='24px'>
<Box marginBlockEnd='16px'>
<Box fontScale='p2m' marginBlockEnd='8px'>
{t('Shortcut')}:
</Box>
<Box fontScale='c1' color='hint'>
!{shortcut}
</Box>
</Box>
<Box marginBlockEnd='16px'>
<Box fontScale='p2m' marginBlockEnd='8px'>
{t('Content')}:
</Box>
<Box fontScale='c1' color='hint'>
"{text}"
</Box>
</Box>
<Box marginBlockEnd='16px'>
<Box fontScale='p2m' marginBlockEnd='8px'>
{t('Sharing')}:
</Box>
<Box fontScale='c1' color='hint'>
{scope}
</Box>
</Box>
<Box marginBlockEnd='16px'>
<Box fontScale='p2m' marginBlockEnd='8px'>
{t('Tags')}:
</Box>
<Box display='flex' flexDirection='row'>
{tags && tags.length > 0 ? (
<Box display='flex' width='full' flexDirection='row' marginBlockStart='8px' flexWrap='wrap'>
{tags.map((tag: string, idx: number) => (
<Box key={idx} marginInlineEnd='4px' marginBlockEnd='4px'>
<Tag>{tag}</Tag>
</Box>
))}
</Box>
) : (
'-'
)}
</Box>
</Box>
</Box>
</ContextualbarContent>
<ContextualbarFooter>
<ButtonGroup stretch>
{allowEdit && <Button onClick={onClickEdit}>{t('Edit')}</Button>}
<Button primary disabled={!allowUse} onClick={onClickUse}>
{t('Use')}
</Button>
</ButtonGroup>
</ContextualbarFooter>
</ContextualbarDialog>
);
};
export default memo(CannedResponse);

View File

@@ -0,0 +1,126 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { composeStories } from '@storybook/react';
import { render, screen } from '@testing-library/react';
import { axe } from 'jest-axe';
import CannedResponseList from './CannedResponseList';
import * as stories from './CannedResponseList.stories';
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
// Mock the useRoomToolbox hook
jest.mock('@rocket.chat/ui-contexts', () => ({
...jest.requireActual('@rocket.chat/ui-contexts'),
useRoomToolbox: () => ({
context: undefined,
}),
}));
describe('CannedResponseList', () => {
describe('Storybook Stories', () => {
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const { baseElement } = render(<Story />);
expect(baseElement).toMatchSnapshot();
});
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />);
const results = await axe(container, {
rules: {
'nested-interactive': { enabled: false },
},
});
expect(results).toHaveNoViolations();
});
});
describe('Permission Testing', () => {
const defaultProps = {
loadMoreItems: jest.fn(),
cannedItems: [
{
shortcut: 'test',
text: 'simple canned response test',
scope: 'global',
tags: ['sales', 'support'],
_createdAt: new Date(),
_id: 'test',
_updatedAt: new Date(),
createdBy: {
_id: 'rocket.cat',
username: 'rocket.cat',
},
departmentName: '',
userId: 'rocket.cat',
departmentId: '',
},
],
itemCount: 1,
onClose: jest.fn(),
loading: false,
options: [['all', 'All'] as [string, string], ['global', 'Public'] as [string, string], ['user', 'Private'] as [string, string]],
text: '',
setText: jest.fn(),
type: 'all',
setType: jest.fn(),
isRoomOverMacLimit: false,
onClickItem: jest.fn(),
onClickCreate: jest.fn(),
onClickUse: jest.fn(),
reload: jest.fn(),
};
it('should render Create button when user has save-canned-responses permission', () => {
render(<CannedResponseList {...defaultProps} />, {
wrapper: mockAppRoot()
.withTranslations('en', 'core', {
Create: 'Create',
})
.withPermission('save-canned-responses')
.build(),
});
expect(screen.getByText('Create')).toBeInTheDocument();
});
it('should NOT render Create button when user has only save-department-canned-responses permission', () => {
render(<CannedResponseList {...defaultProps} />, {
wrapper: mockAppRoot()
.withTranslations('en', 'core', {
Create: 'Create',
})
.withPermission('save-department-canned-responses')
.build(),
});
expect(screen.getByText('Create')).toBeInTheDocument();
});
it('should render Create button when user has both permissions', () => {
render(<CannedResponseList {...defaultProps} />, {
wrapper: mockAppRoot()
.withTranslations('en', 'core', {
Create: 'Create',
})
.withPermission('save-canned-responses')
.withPermission('save-department-canned-responses')
.build(),
});
expect(screen.getByText('Create')).toBeInTheDocument();
});
it('should NOT render Create button when user has neither permission', () => {
render(<CannedResponseList {...defaultProps} />, {
wrapper: mockAppRoot()
.withTranslations('en', 'core', {
Create: 'Create',
})
.build(),
});
expect(screen.queryByText('Create')).not.toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,80 @@
import { Contextualbar } from '@rocket.chat/ui-client';
import type { StoryObj, Meta } from '@storybook/react';
import { action } from 'storybook/actions';
import CannedResponseList from './CannedResponseList';
export default {
component: CannedResponseList,
parameters: {
actions: {
argTypesRegex: '^on.*',
},
},
} satisfies Meta<typeof CannedResponseList>;
export const Default: StoryObj<typeof CannedResponseList> = {
name: 'CannedResponseList',
args: {
options: [
['all', 'All'],
['global', 'Public'],
['user', 'Private'],
],
cannedItems: [
{
shortcut: 'test',
text: 'simple canned response test',
scope: 'global',
tags: ['sales', 'support'],
_createdAt: new Date(),
_id: 'test',
_updatedAt: new Date(),
createdBy: {
_id: 'rocket.cat',
username: 'rocket.cat',
},
departmentName: '',
userId: 'rocket.cat',
departmentId: '',
},
{
shortcut: 'test2',
text: 'simple canned response test2',
scope: 'Customer Support',
tags: [],
_createdAt: new Date(),
_id: 'test',
_updatedAt: new Date(),
createdBy: {
_id: 'rocket.cat',
username: 'rocket.cat',
},
departmentName: '',
userId: 'rocket.cat',
departmentId: '',
},
{
shortcut: 'test3 long long long long long long long long long',
text: 'simple canned response test3 long long long long long long long long long long long long long long long longlong long long long long long longlong long long long long long longlong long long long long long longlong long long long long long longlong long long long long long longlong long long long long long long',
scope: 'Customer Support long long long long long long long long long long',
tags: ['sales', 'support', 'long', 'long', 'long', 'long', 'long', 'long', 'long', 'long'],
_createdAt: new Date(),
_id: 'test',
_updatedAt: new Date(),
createdBy: {
_id: 'rocket.cat',
username: 'rocket.cat',
},
departmentName: '',
userId: 'rocket.cat',
departmentId: '',
},
],
itemCount: 3,
loadMoreItems: action('loadMoreItems'),
},
decorators: [(fn) => <Contextualbar height='100vh'>{fn()}</Contextualbar>],
};

View File

@@ -0,0 +1,141 @@
import type { ILivechatDepartment, IOmnichannelCannedResponse } from '@rocket.chat/core-typings';
import { Box, Button, ButtonGroup, Icon, Margins, Select, TextInput } from '@rocket.chat/fuselage';
import { useAutoFocus, useResizeObserver } from '@rocket.chat/fuselage-hooks';
import {
VirtualizedScrollbars,
ContextualbarHeader,
ContextualbarTitle,
ContextualbarClose,
ContextualbarContent,
ContextualbarEmptyContent,
ContextualbarFooter,
ContextualbarDialog,
} from '@rocket.chat/ui-client';
import { useRoomToolbox } from '@rocket.chat/ui-contexts';
import type { Dispatch, ChangeEventHandler, MouseEvent, SetStateAction } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Virtuoso } from 'react-virtuoso';
import Item from './Item';
import WrapCannedResponse from './WrapCannedResponse';
import { useCanCreateCannedResponse } from '../../hooks/useCanCreateCannedResponse';
export type CannedResponseListProps = {
loadMoreItems: () => void;
cannedItems: (IOmnichannelCannedResponse & { departmentName?: ILivechatDepartment['name'] })[];
itemCount: number;
onClose: () => void;
options: [string, string][];
text: string;
setText: ChangeEventHandler<HTMLInputElement>;
type: string;
setType: Dispatch<SetStateAction<string>>;
isRoomOverMacLimit: boolean;
onClickItem: (data: any) => void; // FIXME: fix typings
onClickCreate: (e: MouseEvent<HTMLOrSVGElement>) => void;
onClickUse: (e: MouseEvent<HTMLOrSVGElement>, text: string) => void;
reload: () => void;
};
const CannedResponseList = ({
loadMoreItems,
cannedItems,
itemCount,
onClose,
options,
text,
setText,
type,
setType,
isRoomOverMacLimit,
onClickItem,
onClickCreate,
onClickUse,
reload,
}: CannedResponseListProps) => {
const { t } = useTranslation();
const inputRef = useAutoFocus<HTMLInputElement>(true);
const { context: cannedId } = useRoomToolbox();
const canCreateCannedResponse = useCanCreateCannedResponse();
const { ref, contentBoxSize: { inlineSize = 378 } = {} } = useResizeObserver<HTMLElement>({
debounceDelay: 200,
});
const cannedItem = cannedItems.find((canned) => canned._id === cannedId);
if (cannedItem) {
return (
<WrapCannedResponse
canUseCannedResponses={!isRoomOverMacLimit}
cannedItem={cannedItem}
onClickBack={onClickItem}
onClickUse={onClickUse}
onClose={onClose}
reload={reload}
/>
);
}
return (
<ContextualbarDialog>
<ContextualbarHeader>
<ContextualbarTitle>{t('Canned_Responses')}</ContextualbarTitle>
<ContextualbarClose onClick={onClose} />
</ContextualbarHeader>
<ContextualbarContent paddingInline={0} ref={ref}>
<Box display='flex' flexDirection='row' padding={24} flexShrink={0}>
<Box display='flex' flexDirection='row' flexGrow={1} marginInline='neg-x4'>
<Margins inline={4}>
<TextInput
placeholder={t('Search')}
value={text}
onChange={setText}
endAddon={<Icon name='magnifier' size='x20' />}
ref={inputRef}
/>
<Box width='x144'>
<Select aria-label={t('Type')} onChange={(value) => setType(String(value))} value={type} options={options} />
</Box>
</Margins>
</Box>
</Box>
{itemCount === 0 && <ContextualbarEmptyContent title={t('No_Canned_Responses')} />}
{itemCount > 0 && cannedItems.length > 0 && (
<Box flexGrow={1} flexShrink={1} overflow='hidden' display='flex'>
<VirtualizedScrollbars>
<Virtuoso
style={{ width: inlineSize }}
totalCount={itemCount}
endReached={loadMoreItems}
overscan={25}
data={cannedItems}
itemContent={(_index, data) => (
<Item
data={data}
allowUse={!isRoomOverMacLimit}
onClickItem={(): void => {
onClickItem(data);
}}
onClickUse={onClickUse}
/>
)}
/>
</VirtualizedScrollbars>
</Box>
)}
</ContextualbarContent>
{canCreateCannedResponse && (
<ContextualbarFooter>
<ButtonGroup stretch>
<Button onClick={onClickCreate}>{t('Create')}</Button>
</ButtonGroup>
</ContextualbarFooter>
)}
</ContextualbarDialog>
);
};
export default memo(CannedResponseList);

View File

@@ -0,0 +1,38 @@
import { Box } from '@rocket.chat/fuselage';
import type { StoryObj, Meta } from '@storybook/react';
import Item from './Item';
export default {
component: Item,
parameters: {
actions: {
argTypesRegex: '^on.*',
},
},
} satisfies Meta<typeof Item>;
export const Default: StoryObj<typeof Item> = {
name: 'Item',
args: {
data: {
shortcut: 'test',
text: 'simple canned response test',
scope: 'global',
tags: ['sales', 'support'],
_createdAt: new Date(),
_id: 'test',
_updatedAt: new Date(),
createdBy: {
_id: 'rocket.cat',
username: 'rocket.cat',
},
departmentName: '',
userId: 'rocket.cat',
departmentId: '',
},
},
decorators: [(fn) => <Box width='330px'>{fn()}</Box>],
};

View File

@@ -0,0 +1,80 @@
import type { ILivechatDepartment, IOmnichannelCannedResponse } from '@rocket.chat/core-typings';
import { css } from '@rocket.chat/css-in-js';
import { Box, Button, Icon, Tag } from '@rocket.chat/fuselage';
import type { MouseEvent } from 'react';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useScopeDict } from '../../../hooks/useScopeDict';
export type ItemProps = {
data: IOmnichannelCannedResponse & { departmentName?: ILivechatDepartment['name'] };
allowUse?: boolean;
onClickItem: (e: MouseEvent<HTMLOrSVGElement>) => void;
onClickUse: (e: MouseEvent<HTMLOrSVGElement>, text: string) => void;
};
const Item = ({ data, allowUse, onClickItem, onClickUse }: ItemProps) => {
const { t } = useTranslation();
const scope = useScopeDict(data.scope, data.departmentName);
const clickable = css`
cursor: pointer;
`;
const [visibility, setVisibility] = useState(false);
return (
<Box
paddingBlockStart={16}
paddingBlockEnd={12}
paddingInline={24}
color='default'
borderBlockEndWidth={1}
borderBlockEndColor='light'
borderBlockEndStyle='solid'
onClick={onClickItem}
className={clickable}
onMouseEnter={(): void => setVisibility(true)}
onMouseLeave={(): void => setVisibility(false)}
>
<Box display='flex' flexDirection='row' justifyContent='space-between' alignItems='center'>
<Box width='full' minWidth={0}>
<Box fontScale='p2m' withTruncatedText>
!{data.shortcut}
</Box>
<Box fontScale='c1' color='hint' withTruncatedText>
{scope}
</Box>
</Box>
<Box display='flex' flexDirection='row' alignItems='center'>
<Button
display={visibility && allowUse ? 'block' : 'none'}
small
onClick={(e): void => {
onClickUse(e, data.text);
}}
>
{t('Use')}
</Button>
<Icon name='chevron-left' size='x24' color='hint' />
</Box>
</Box>
<Box fontScale='p2' marginBlockStart='8px' color='hint' withTruncatedText>
"{data.text}"
</Box>
{data.tags && data.tags.length > 0 && (
<Box display='flex' width='full' flexDirection='row' marginBlockStart='8px' flexWrap='wrap'>
{data.tags.map((tag: string, idx: number) => (
<Box key={idx} marginInlineEnd='4px' marginBlockEnd='4px'>
<Tag>{tag}</Tag>
</Box>
))}
</Box>
)}
</Box>
);
};
export default memo(Item);

View File

@@ -0,0 +1,42 @@
import type { ILivechatDepartment, IOmnichannelCannedResponse } from '@rocket.chat/core-typings';
import { useSetModal } from '@rocket.chat/ui-contexts';
import type { MouseEvent, MouseEventHandler } from 'react';
import { memo } from 'react';
import CannedResponse from './CannedResponse';
import { useCanEditCannedResponse } from '../../hooks/useCanEditCannedResponse';
import CreateCannedResponse from '../../modals/CreateCannedResponse';
export type WrapCannedResponseProps = {
canUseCannedResponses: boolean;
cannedItem: IOmnichannelCannedResponse & { departmentName?: ILivechatDepartment['name'] };
onClickBack: MouseEventHandler<HTMLOrSVGElement>;
onClickUse: (e: MouseEvent<HTMLOrSVGElement>, text: string) => void;
onClose: () => void;
reload: () => void;
};
const WrapCannedResponse = ({ canUseCannedResponses, cannedItem, onClickBack, onClose, onClickUse, reload }: WrapCannedResponseProps) => {
const setModal = useSetModal();
const onClickEdit = (): void => {
setModal(<CreateCannedResponse cannedResponseData={cannedItem} onClose={() => setModal(null)} reloadCannedList={reload} />);
};
const canEditCannedResponses = useCanEditCannedResponse(cannedItem);
return (
<CannedResponse
allowEdit={canEditCannedResponses}
allowUse={canUseCannedResponses}
data={cannedItem}
onClickBack={onClickBack}
onClickEdit={onClickEdit}
onClose={onClose}
onClickUse={(e): void => {
onClickUse(e, cannedItem?.text);
}}
/>
);
};
export default memo(WrapCannedResponse);

View File

@@ -0,0 +1,90 @@
import type { IOmnichannelCannedResponse, ILivechatDepartment } from '@rocket.chat/core-typings';
import { useDebouncedValue, useLocalStorage, useStableCallback } from '@rocket.chat/fuselage-hooks';
import { useSetModal, useRouter, useRoomToolbox } from '@rocket.chat/ui-contexts';
import type { ChangeEvent, MouseEvent } from 'react';
import { memo, useCallback, useState } from 'react';
import CannedResponseList from './CannedResponseList';
import { useChat } from '../../../../room/contexts/ChatContext';
import { useRoom } from '../../../../room/contexts/RoomContext';
import { useCannedResponseFilterOptions } from '../../../hooks/useCannedResponseFilterOptions';
import { useCannedResponseList } from '../../../hooks/useCannedResponseList';
import { useIsRoomOverMacLimit } from '../../../hooks/useIsRoomOverMacLimit';
import CreateCannedResponse from '../../modals/CreateCannedResponse';
export const WrapCannedResponseList = () => {
const room = useRoom();
const { closeTab } = useRoomToolbox();
const router = useRouter();
const setModal = useSetModal();
const options = useCannedResponseFilterOptions() as [string, string][];
const [text, setText] = useState('');
const [type, setType] = useLocalStorage('canned-response-list-type', 'all');
const isRoomOverMacLimit = useIsRoomOverMacLimit(room);
const handleTextChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setText(event.currentTarget.value);
}, []);
const debouncedText = useDebouncedValue(text, 400);
// TODO: handle pending and error states
const { data, fetchNextPage, refetch } = useCannedResponseList({ filter: debouncedText, type });
const onClickItem = useStableCallback(
(
data: IOmnichannelCannedResponse & {
departmentName: ILivechatDepartment['name'];
},
) => {
const { _id: context } = data;
router.navigate({
name: router.getRouteName() ?? 'live',
params: {
id: room._id,
tab: 'canned-responses',
context,
},
});
},
);
const composer = useChat()?.composer;
const onClickUse = (e: MouseEvent<HTMLOrSVGElement>, text: string): void => {
e.preventDefault();
e.stopPropagation();
composer?.setText(text);
composer?.focus();
};
const onClickCreate = (): void => {
setModal(<CreateCannedResponse onClose={() => setModal(null)} reloadCannedList={refetch} />);
};
return (
<CannedResponseList
loadMoreItems={fetchNextPage}
cannedItems={data?.cannedItems ?? []}
itemCount={data?.total ?? 0}
onClose={closeTab}
options={options}
text={text}
setText={handleTextChange}
type={type}
setType={setType}
isRoomOverMacLimit={isRoomOverMacLimit}
onClickUse={onClickUse}
onClickItem={onClickItem}
onClickCreate={onClickCreate}
reload={refetch}
/>
);
};
export default memo(WrapCannedResponseList);

View File

@@ -0,0 +1,201 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`CannedResponseList Storybook Stories renders Default without crashing 1`] = `
<body>
<div>
<div
class="rcx-box rcx-box--full rcx-vertical-bar rcx-css-glyll0"
>
<span
data-focus-scope-start="true"
hidden=""
/>
<div
style="position: relative; user-select: auto; width: 380px; height: 100%; max-width: 50%; min-width: 380px; min-height: 100%; box-sizing: border-box; flex-shrink: 0;"
>
<div
aria-labelledby="contextualbarTitle"
class="rcx-box rcx-box--full rcx-vertical-bar rcx-css-nlfrt3"
role="dialog"
tabindex="-1"
>
<div
class="rcx-box rcx-box--full rcx-css-lm07yg"
>
<div
class="rcx-box rcx-box--full rcx-css-1bdyjik rcx-css-1l4fxfb"
>
<h5
class="rcx-box rcx-box--full rcx-css-1spko5i"
id="contextualbarTitle"
>
Canned_Responses
</h5>
<button
aria-label="Close"
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--square rcx-button--icon rcx-button rcx-css-ue04py"
data-qa="ContextualbarActionClose"
type="button"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-cross rcx-icon rcx-css-4pvxx3"
>
</i>
</button>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-vertical-bar__content rcx-css-10ikbhr"
>
<div
class="rcx-box rcx-box--full rcx-css-cb0t7p"
>
<div
class="rcx-box rcx-box--full rcx-css-t6z7xh"
>
<label
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper rcx-css-1dtwr38 rcx-css-1eogw2f"
>
<input
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-text rcx-input-box rcx-css-1dtwr38"
placeholder="Search"
size="1"
type="text"
/>
<span
class="rcx-box rcx-box--full rcx-input-box__addon"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-magnifier rcx-icon rcx-css-4pvxx3"
>
</i>
</span>
</label>
<div
class="rcx-box rcx-box--full rcx-css-1dtwr38 rcx-css-njvvp7"
>
<div
class="rcx-box rcx-box--full rcx-css-1mbgoc8"
>
<div
aria-hidden="true"
data-a11y-ignore="aria-hidden-focus"
data-react-aria-prevent-focus="true"
data-testid="hidden-select-container"
style="border: 0px; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
>
<label>
<select
tabindex="-1"
>
<option />
<option
value="all"
>
all
</option>
<option
value="global"
>
global
</option>
<option
value="user"
>
user
</option>
</select>
</label>
</div>
</div>
<button
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Type"
aria-labelledby="react-aria-_r_8_ react-aria-_r_3_"
class="rcx-box rcx-box--full rcx-box--animated rcx-select rcx-css-1vw6rc6"
type="button"
>
<span
class="rcx-box rcx-box--full rcx-css-az37y0"
id="react-aria-_r_8_"
/>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-chevron-down rcx-icon rcx-css-l5txo2"
>
</i>
</button>
</div>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-om2mbq"
>
<div
class="rcx-box rcx-box--full rcx-css-1pd039f rcx-css-1cb6i7s"
>
<div
data-testid="virtuoso-scroller"
data-virtuoso-scroller="true"
style="height: 100%; outline: none; overflow-y: auto; position: relative; width: 378px;"
tabindex="-1"
>
<div
data-viewport-type="element"
style="height: 100%; position: absolute; top: 0px; width: 100%;"
>
<div
data-testid="virtuoso-item-list"
style="box-sizing: border-box; margin-top: 0px; padding-bottom: 0px; padding-top: 0px;"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<div
style="position: absolute; user-select: none; width: 100%; height: 10px; top: -5px; left: 0px; cursor: row-resize;"
/>
<div
style="position: absolute; user-select: none; width: 10px; height: 100%; top: 0px; cursor: col-resize; right: -5px;"
/>
<div
style="position: absolute; user-select: none; width: 100%; height: 10px; left: 0px; cursor: row-resize; bottom: -5px;"
/>
<div
style="position: absolute; user-select: none; width: 3px; height: 100%; top: 0px; left: 0px; cursor: col-resize; z-index: 99;"
>
<div
class="rcx-box rcx-box--full rcx-css-124vum9"
/>
</div>
<div
style="position: absolute; user-select: none; width: 20px; height: 20px; z-index: 1; right: -10px; top: -10px; cursor: ne-resize;"
/>
<div
style="position: absolute; user-select: none; width: 20px; height: 20px; z-index: 1; right: -10px; bottom: -10px; cursor: se-resize;"
/>
<div
style="position: absolute; user-select: none; width: 20px; height: 20px; z-index: 1; left: -10px; bottom: -10px; cursor: sw-resize;"
/>
<div
style="position: absolute; user-select: none; width: 20px; height: 20px; z-index: 1; left: -10px; top: -10px; cursor: nw-resize;"
/>
</div>
</div>
<span
data-focus-scope-end="true"
hidden=""
/>
</div>
</div>
</body>
`;