Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance Message Component to Support ReactNode in Content via extraContent Prop #419

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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 @@ -19,6 +19,7 @@ propComponents:
'FileDropZone',
'PreviewAttachment',
'Message',
'MessageExtraContent',
'PreviewAttachment',
'ActionProps',
'SourcesCardProps'
Expand Down Expand Up @@ -135,6 +136,14 @@ Messages from users have a different background color to differentiate them from

```

### User messages with extraContent prop

The `extraContent` prop makes the `<Message>` component more flexible by letting you add extra content in specific parts of a message. This is useful for adding things like timestamps, badges, or custom elements without changing the default layout. This allows you to create dynamic and reusable elements for various use cases.

```js file="./UserMessageWithExtraContent.tsx"

```

## File attachments

### Messages with attachments
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';

import Message from '@patternfly/chatbot/dist/dynamic/Message';
import userAvatar from './user_avatar.svg';
import { Alert, Badge, Button, Card, CardBody, CardFooter, CardTitle } from '@patternfly/react-core';

const UserActionEndContent = () => {
// eslint-disable-next-line no-console
const onClick = () => console.log('custom button click');
return (
<React.Fragment>
<Button variant="secondary" ouiaId="Secondary" onClick={onClick}>
End content button
</Button>
<Alert variant="danger" title="Danger alert title" ouiaId="DangerAlert" />
</React.Fragment>
);
};

const CardInformationAfterMainContent = () => (
<Card ouiaId="BasicCard">
<CardTitle>This is content card after main content</CardTitle>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>
);

const BeforeMainContent = () => (
<div>
<Badge key={1} isRead>
7
</Badge>
<Badge key={2} isRead>
24
</Badge>
</div>
);

export const UserMessageWithExtraContent: React.FunctionComponent = () => (
<>
<Message
avatar={userAvatar}
name="User"
role="user"
content="This is a main message."
timestamp="1 hour ago"
extraContent={{
beforeMainContent: <BeforeMainContent />,
afterMainContent: <CardInformationAfterMainContent />,
endContent: <UserActionEndContent />
}}
/>
</>
);
114 changes: 114 additions & 0 deletions packages/module/src/Message/Message.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,118 @@ describe('Message', () => {
);
expect(screen.getAllByRole('img')[1]).toHaveAttribute('src', 'test.png');
});
it('should render beforeMainContent with main content', () => {
const mainContent = 'Main message content';
const beforeMainContentText = 'Before main content';
const beforeMainContent = <div>{beforeMainContentText}</div>;

render(
<Message avatar="./img" role="user" name="User" content={mainContent} extraContent={{ beforeMainContent }} />
);

expect(screen.getByText(beforeMainContentText)).toBeTruthy();
expect(screen.getByText(mainContent)).toBeTruthy();
});
it('should render afterMainContent with main content', () => {
const mainContent = 'Main message content';
const afterMainContentText = 'After main content';
const afterMainContent = <div>{afterMainContentText}</div>;

render(
<Message avatar="./img" role="user" name="User" content={mainContent} extraContent={{ afterMainContent }} />
);

expect(screen.getByText(afterMainContentText)).toBeTruthy();
expect(screen.getByText(mainContent)).toBeTruthy();
});

it('should render endContent with main content', () => {
const mainContent = 'Main message content';
const endMainContentText = 'End content';
const endContent = <div>{endMainContentText}</div>;

render(<Message avatar="./img" role="user" name="User" content={mainContent} extraContent={{ endContent }} />);

expect(screen.getByText(endMainContentText)).toBeTruthy();
expect(screen.getByText(mainContent)).toBeTruthy();
});
it('should render all parts of extraContent with main content', () => {
const beforeMainContent = <div>Before main content</div>;
const afterMainContent = <div>After main content</div>;
const endContent = <div>End content</div>;

render(
<Message
avatar="./img"
role="user"
name="User"
content="Main message content"
extraContent={{ beforeMainContent, afterMainContent, endContent }}
/>
);

expect(screen.getByText('Before main content')).toBeTruthy();
expect(screen.getByText('Main message content')).toBeTruthy();
expect(screen.getByText('After main content')).toBeTruthy();
expect(screen.getByText('End content')).toBeTruthy();
});

it('should not render extraContent when not provided', () => {
render(<Message avatar="./img" role="user" name="User" content="Main message content" />);

// Ensure no extraContent is rendered
expect(screen.getByText('Main message content')).toBeTruthy();
expect(screen.queryByText('Before main content')).toBeFalsy();
expect(screen.queryByText('After main content')).toBeFalsy();
expect(screen.queryByText('end message content')).toBeFalsy();
});

it('should handle undefined or null values in extraContent gracefully', () => {
render(
<Message
avatar="./img"
role="user"
name="User"
content="Main message content"
extraContent={{ beforeMainContent: null, afterMainContent: undefined, endContent: null }}
/>
);

// Ensure that no extraContent is rendered if they are null or undefined
expect(screen.getByText('Main message content')).toBeTruthy();
expect(screen.queryByText('Before main content')).toBeFalsy();
expect(screen.queryByText('After main content')).toBeFalsy();
expect(screen.queryByText('end message content')).toBeFalsy();
});
it('should render JSX in extraContent correctly', () => {
const beforeMainContent = (
<div data-testid="before-main-content">
<strong>Bold before content</strong>
</div>
);
const afterMainContent = (
<div data-testid="after-main-content">
<strong>Bold after content</strong>
</div>
);
const endContent = (
<div data-testid="end-main-content">
<strong>Bold end content</strong>
</div>
);
render(
<Message
avatar="./img"
role="user"
name="User"
content="Main message content"
extraContent={{ beforeMainContent, afterMainContent, endContent }}
/>
);

// Check that the JSX is correctly rendered
expect(screen.getByTestId('before-main-content')).toContainHTML('<strong>Bold before content</strong>');
expect(screen.getByTestId('after-main-content')).toContainHTML('<strong>Bold after content</strong>');
expect(screen.getByTestId('end-main-content')).toContainHTML('<strong>Bold end content</strong>');
});
});
46 changes: 33 additions & 13 deletions packages/module/src/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Chatbot Main - Message
// ============================================================================

import React from 'react';
import React, { ReactNode } from 'react';

import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
Expand Down Expand Up @@ -39,13 +39,26 @@ export interface MessageAttachment {
spinnerTestId?: string;
}

export interface MessageExtraContent {
/** Content to display before the main content */
beforeMainContent?: ReactNode;

/** Content to display after the main content */
afterMainContent?: ReactNode;

/** Content to display at the end */
endContent?: ReactNode;
}

export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'role'> {
/** Unique id for message */
id?: string;
/** Role of the user sending the message */
role: 'user' | 'bot';
/** Message content */
content?: string;
/** Extra Message content */
extraContent?: MessageExtraContent;
/** Name of the user */
name?: string;
/** Avatar src for the user */
Expand Down Expand Up @@ -96,6 +109,7 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
export const Message: React.FunctionComponent<MessageProps> = ({
role,
content,
extraContent,
name,
avatar,
timestamp,
Expand All @@ -113,6 +127,7 @@ export const Message: React.FunctionComponent<MessageProps> = ({
quickStarts,
...props
}: MessageProps) => {
const { beforeMainContent, afterMainContent, endContent } = extraContent || {};
let avatarClassName;
if (avatarProps && 'className' in avatarProps) {
const { className, ...rest } = avatarProps;
Expand Down Expand Up @@ -155,18 +170,22 @@ export const Message: React.FunctionComponent<MessageProps> = ({
{isLoading ? (
<MessageLoading loadingWord={loadingWord} />
) : (
<Markdown
components={{
p: TextMessage,
code: ({ children }) => <CodeBlockMessage {...codeBlockProps}>{children}</CodeBlockMessage>,
ul: UnorderedListMessage,
ol: (props) => <OrderedListMessage {...props} />,
li: ListItemMessage
}}
remarkPlugins={[remarkGfm]}
>
{content}
</Markdown>
<>
{beforeMainContent && <>{beforeMainContent}</>}
<Markdown
components={{
p: TextMessage,
code: ({ children }) => <CodeBlockMessage {...codeBlockProps}>{children}</CodeBlockMessage>,
ul: UnorderedListMessage,
ol: (props) => <OrderedListMessage {...props} />,
li: ListItemMessage
}}
remarkPlugins={[remarkGfm]}
>
{content}
</Markdown>
{afterMainContent && <>{afterMainContent}</>}
</>
)}
{!isLoading && sources && <SourcesCard {...sources} />}
{quickStarts && quickStarts.quickStart && (
Expand Down Expand Up @@ -206,6 +225,7 @@ export const Message: React.FunctionComponent<MessageProps> = ({
))}
</div>
)}
{!isLoading && endContent && <>{endContent}</>}
</div>
</div>
</section>
Expand Down