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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import Message from '@patternfly/chatbot/dist/dynamic/Message';
import patternflyAvatar from './patternfly_avatar.jpg';
import squareImg from './PF-social-color-square.svg';
import { Form, FormGroup, Radio } from '@patternfly/react-core';
import { AlertActionLink, Form, FormGroup, Radio } from '@patternfly/react-core';

export const BotMessageExample: React.FunctionComponent = () => {
const [variant, setVariant] = React.useState('code');
Expand Down Expand Up @@ -140,6 +140,21 @@ _Italic text, formatted with single underscores_

const image = `![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`;

const error = {
title: 'Could not load chat',
children: 'Wait a few minutes and check your network settings. If the issue persists: ',
actionLinks: (
<React.Fragment>
<AlertActionLink component="a" href="#">
Start a new chat
</AlertActionLink>
<AlertActionLink component="a" href="#">
Contact support
</AlertActionLink>
</React.Fragment>
)
};

return (
<>
<Message
Expand Down Expand Up @@ -258,6 +273,13 @@ _Italic text, formatted with single underscores_
label="Image"
id="image"
/>
<Radio
isChecked={variant === 'error'}
onChange={() => setVariant('error')}
name="bot-message-error"
label="Error"
id="error"
/>
</FormGroup>
</Form>
<Message
Expand All @@ -268,6 +290,7 @@ _Italic text, formatted with single underscores_
tableProps={
variant === 'table' ? { 'aria-label': 'App information and user roles for bot messages' } : undefined
}
error={variant === 'error' ? error : undefined}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';

import Message from '@patternfly/chatbot/dist/dynamic/Message';
import userAvatar from './user_avatar.svg';
import { Form, FormGroup, Radio } from '@patternfly/react-core';
import { AlertActionLink, Form, FormGroup, Radio } from '@patternfly/react-core';

export const UserMessageExample: React.FunctionComponent = () => {
const [variant, setVariant] = React.useState('code');
Expand Down Expand Up @@ -140,6 +140,21 @@ _Italic text, formatted with single underscores_

const image = `![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`;

const error = {
title: 'Could not load chat',
children: 'Wait a few minutes and check your network settings. If the issue persists: ',
actionLinks: (
<React.Fragment>
<AlertActionLink component="a" href="#">
Start a new chat
</AlertActionLink>
<AlertActionLink component="a" href="#">
Contact support
</AlertActionLink>
</React.Fragment>
)
};

return (
<>
<Message
Expand Down Expand Up @@ -235,6 +250,13 @@ _Italic text, formatted with single underscores_
label="Image"
id="user-image"
/>
<Radio
isChecked={variant === 'error'}
onChange={() => setVariant('error')}
name="user-message-error"
label="Error"
id="user-error"
/>
</FormGroup>
</Form>
<Message
Expand All @@ -245,6 +267,7 @@ _Italic text, formatted with single underscores_
tableProps={
variant === 'table' ? { 'aria-label': 'App information and user roles for user messages' } : undefined
}
error={variant === 'error' ? error : undefined}
/>
</>
);
Expand Down
14 changes: 14 additions & 0 deletions packages/module/src/Message/ErrorMessage/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// ============================================================================
// Chatbot Main - Message - Content - Error
// ============================================================================

import React from 'react';
import { Alert, AlertProps } from '@patternfly/react-core';

const ErrorMessage = ({ title, actionLinks, children, ...props }: AlertProps) => (
<Alert isInline variant="danger" title={title} actionLinks={actionLinks} {...props}>
{children}
</Alert>
);

export default ErrorMessage;
32 changes: 32 additions & 0 deletions packages/module/src/Message/Message.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import userEvent from '@testing-library/user-event';
import { monitorSampleAppQuickStart } from './QuickStarts/monitor-sampleapp-quickstart';
import { monitorSampleAppQuickStartWithImage } from './QuickStarts/monitor-sampleapp-quickstart-with-image';
import rehypeExternalLinks from '../__mocks__/rehype-external-links';
import { AlertActionLink } from '@patternfly/react-core';

const ALL_ACTIONS = [
{ label: /Good response/i },
Expand Down Expand Up @@ -141,6 +142,20 @@ const EMPTY_TABLE = `

const IMAGE = `![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`;

const ERROR = {
title: 'Could not load chat',
children: 'Wait a few minutes and check your network settings. If the issue persists: ',
actionLinks: (
<React.Fragment>
<AlertActionLink component="a" href="#">
Start a new chat
</AlertActionLink>
<AlertActionLink component="a" href="#">
Contact support
</AlertActionLink>
</React.Fragment>
)
};
const checkListItemsRendered = () => {
const items = ['Item 1', 'Item 2', 'Item 3'];
expect(screen.getAllByRole('listitem')).toHaveLength(3);
Expand Down Expand Up @@ -769,4 +784,21 @@ describe('Message', () => {
// we are mocking rehype libraries, so we can't test target _blank addition on links directly with RTL
expect(rehypeExternalLinks).not.toHaveBeenCalled();
});
it('should handle error correctly', () => {
render(<Message avatar="./img" role="user" name="User" error={ERROR} />);
expect(screen.getByRole('heading', { name: /Could not load chat/i })).toBeTruthy();
expect(screen.getByRole('link', { name: /Start a new chat/i })).toBeTruthy();
expect(screen.getByRole('link', { name: /Contact support/i })).toBeTruthy();
expect(screen.getByText('Wait a few minutes and check your network settings. If the issue persists:')).toBeTruthy();
});
it('should handle error correctly when loading', () => {
render(<Message avatar="./img" role="user" name="User" error={ERROR} isLoading />);
expect(screen.queryByRole('heading', { name: /Could not load chat/i })).toBeFalsy();
expect(screen.getByText('Loading message')).toBeTruthy();
});
it('should handle error correctly when these is content', () => {
render(<Message avatar="./img" role="user" name="User" error={ERROR} content="Test" />);
expect(screen.getByRole('heading', { name: /Could not load chat/i })).toBeTruthy();
expect(screen.queryByText('Test')).toBeFalsy();
});
});
91 changes: 50 additions & 41 deletions packages/module/src/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React, { ReactNode } from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import {
AlertProps,
Avatar,
AvatarProps,
ContentVariants,
Expand Down Expand Up @@ -42,6 +43,7 @@ import rehypeExternalLinks from 'rehype-external-links';
import rehypeSanitize from 'rehype-sanitize';
import { PluggableList } from 'react-markdown/lib';
import LinkMessage from './LinkMessage/LinkMessage';
import ErrorMessage from './ErrorMessage/ErrorMessage';

export interface MessageAttachment {
/** Name of file attached to the message */
Expand Down Expand Up @@ -141,6 +143,8 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
additionalRehypePlugins?: PluggableList;
/** Whether to open links in message in new tab. */
openLinkInNewTab?: boolean;
/** Optional inline error message that can be displayed in the message */
error?: AlertProps;
}

export const MessageBase: React.FunctionComponent<MessageProps> = ({
Expand Down Expand Up @@ -169,6 +173,7 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
tableProps,
openLinkInNewTab = true,
additionalRehypePlugins = [],
error,
...props
}: MessageProps) => {
const { beforeMainContent, afterMainContent, endContent } = extraContent || {};
Expand Down Expand Up @@ -225,47 +230,51 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
) : (
<>
{beforeMainContent && <>{beforeMainContent}</>}
<Markdown
components={{
p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
code: ({ children, ...props }) => (
<CodeBlockMessage {...props} {...codeBlockProps}>
{children}
</CodeBlockMessage>
),
h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
ul: (props) => <UnorderedListMessage {...props} />,
ol: (props) => <OrderedListMessage {...props} />,
li: (props) => <ListItemMessage {...props} />,
table: (props) => <TableMessage {...props} {...tableProps} />,
tbody: (props) => <TbodyMessage {...props} />,
thead: (props) => <TheadMessage {...props} />,
tr: (props) => <TrMessage {...props} />,
td: (props) => {
// Conflicts with Td type
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { width, ...rest } = props;
return <TdMessage {...rest} />;
},
th: (props) => <ThMessage {...props} />,
img: (props) => <ImageMessage {...props} />,
a: (props) => (
<LinkMessage href={props.href} rel={props.rel} target={props.target}>
{props.children}
</LinkMessage>
)
}}
remarkPlugins={[remarkGfm]}
rehypePlugins={rehypePlugins}
>
{content}
</Markdown>
{error ? (
<ErrorMessage {...error} />
) : (
<Markdown
components={{
p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
code: ({ children, ...props }) => (
<CodeBlockMessage {...props} {...codeBlockProps}>
{children}
</CodeBlockMessage>
),
h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
ul: (props) => <UnorderedListMessage {...props} />,
ol: (props) => <OrderedListMessage {...props} />,
li: (props) => <ListItemMessage {...props} />,
table: (props) => <TableMessage {...props} {...tableProps} />,
tbody: (props) => <TbodyMessage {...props} />,
thead: (props) => <TheadMessage {...props} />,
tr: (props) => <TrMessage {...props} />,
td: (props) => {
// Conflicts with Td type
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { width, ...rest } = props;
return <TdMessage {...rest} />;
},
th: (props) => <ThMessage {...props} />,
img: (props) => <ImageMessage {...props} />,
a: (props) => (
<LinkMessage href={props.href} rel={props.rel} target={props.target}>
{props.children}
</LinkMessage>
)
}}
remarkPlugins={[remarkGfm]}
rehypePlugins={rehypePlugins}
>
{content}
</Markdown>
)}
{afterMainContent && <>{afterMainContent}</>}
</>
)}
Expand Down