Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/shaggy-lobsters-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
"@rocket.chat/meteor": patch
---
Fixes canned messages contextual bar "Create" button not being affected by the correct permission
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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('../../../../views/room/contexts/RoomToolboxContext', () => ({
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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '../../../../components/Contextualbar';
import { VirtualizedScrollbars } from '../../../../components/CustomScrollbars';
import { useRoomToolbox } from '../../../../views/room/contexts/RoomToolboxContext';
import { useCanCreateCannedResponse } from '../../hooks/useCanCreateCannedResponse';

type CannedResponseListProps = {
loadMoreItems: (start: number, end: number) => void;
Expand Down Expand Up @@ -58,6 +59,7 @@ const CannedResponseList = ({
const inputRef = useAutoFocus<HTMLInputElement>(true);

const { context: cannedId } = useRoomToolbox();
const canCreateCannedResponse = useCanCreateCannedResponse();

const { ref, contentBoxSize: { inlineSize = 378 } = {} } = useResizeObserver<HTMLElement>({
debounceDelay: 200,
Expand All @@ -68,7 +70,7 @@ const CannedResponseList = ({
if (cannedItem) {
return (
<WrapCannedResponse
allowUse={!isRoomOverMacLimit}
canUseCannedResponses={!isRoomOverMacLimit}
cannedItem={cannedItem}
onClickBack={onClickItem}
onClickUse={onClickUse}
Expand Down Expand Up @@ -96,7 +98,7 @@ const CannedResponseList = ({
ref={inputRef}
/>
<Box w='x144'>
<Select onChange={(value) => setType(String(value))} value={type} options={options} />
<Select aria-label={t('Type')} onChange={(value) => setType(String(value))} value={type} options={options} />
</Box>
</Margins>
</Box>
Expand Down Expand Up @@ -126,11 +128,13 @@ const CannedResponseList = ({
</Box>
)}
</ContextualbarContent>
<ContextualbarFooter>
<ButtonGroup stretch>
<Button onClick={onClickCreate}>{t('Create')}</Button>
</ButtonGroup>
</ContextualbarFooter>
{canCreateCannedResponse && (
<ContextualbarFooter>
<ButtonGroup stretch>
<Button onClick={onClickCreate}>{t('Create')}</Button>
</ButtonGroup>
</ContextualbarFooter>
)}
</ContextualbarDialog>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
import type { ILivechatDepartment, IOmnichannelCannedResponse } from '@rocket.chat/core-typings';
import { useSetModal, usePermission } from '@rocket.chat/ui-contexts';
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';

type WrapCannedResponseProps = {
allowUse: boolean;
canUseCannedResponses: boolean;
cannedItem: IOmnichannelCannedResponse & { departmentName: ILivechatDepartment['name'] };
onClickBack: MouseEventHandler<HTMLOrSVGElement>;
onClickUse: (e: MouseEvent<HTMLOrSVGElement>, text: string) => void;
onClose: () => void;
reload: () => void;
};

const WrapCannedResponse = ({ allowUse, cannedItem, onClickBack, onClose, onClickUse, reload }: WrapCannedResponseProps) => {
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 hasManagerPermission = usePermission('view-all-canned-responses');
const hasMonitorPermission = usePermission('save-department-canned-responses');

const allowEdit = hasManagerPermission || (hasMonitorPermission && cannedItem.scope !== 'global') || cannedItem.scope === 'user';
const canEditCannedResponses = useCanEditCannedResponse(cannedItem);

return (
<CannedResponse
allowEdit={allowEdit}
allowUse={allowUse}
allowEdit={canEditCannedResponses}
allowUse={canUseCannedResponses}
data={cannedItem}
onClickBack={onClickBack}
onClickEdit={onClickEdit}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// 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-css-1kmfepv"
>
<div
class="rcx-box rcx-box--full rcx-vertical-bar rcx-css-8umsxh"
>
<span
data-focus-scope-start="true"
hidden=""
/>
<div
aria-labelledby="contextualbarTitle"
class="rcx-box rcx-box--full rcx-vertical-bar rcx-css-1n0hsd8"
role="dialog"
tabindex="-1"
>
<div
class="rcx-box rcx-box--full rcx-css-ftwpdg"
>
<div
class="rcx-box rcx-box--full rcx-css-1sl6k6j"
>
<div
class="rcx-box rcx-box--full rcx-css-x7bl3q rcx-css-1to6ka7"
id="contextualbarTitle"
>
Canned_Responses
</div>
<button
aria-label="Close"
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--square rcx-button--icon rcx-button rcx-css-x7bl3q rcx-css-1yzvz7u"
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"
value=""
/>
<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"
>
<button
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Type"
aria-labelledby="react-aria-:r8: react-aria-:r3:"
class="rcx-box rcx-box--full rcx-select rcx-css-1vw6rc6"
type="button"
>
<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>
<span
class="rcx-box rcx-box--full rcx-css-uyvtjh"
id="react-aria-:r8:"
/>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-chevron-down rcx-icon rcx-css-6vi44e"
>
</i>
</button>
</div>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-om2mbq"
>
<div
class="rcx-box rcx-box--full rcx-css-pln26h 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="width: 100%; height: 100%; position: absolute; top: 0px;"
>
<div
data-testid="virtuoso-item-list"
style="box-sizing: border-box; margin-top: 0px; padding-top: 0px; padding-bottom: 0px;"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<span
data-focus-scope-end="true"
hidden=""
/>
</div>
</div>
</div>
</body>
`;
Loading
Loading