@patternfly/chatbot 6.4.0-prerelease.23 → 6.4.0-prerelease.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Message/Message.d.ts +2 -0
- package/dist/cjs/Message/Message.js +6 -2
- package/dist/cjs/Message/Message.test.js +17 -2
- package/dist/cjs/SourcesCard/SourcesCard.js +6 -30
- package/dist/cjs/SourcesCard/SourcesCard.test.js +2 -211
- package/dist/cjs/SourcesCardBase/SourcesCardBase.d.ts +57 -0
- package/dist/cjs/SourcesCardBase/SourcesCardBase.js +49 -0
- package/dist/cjs/SourcesCardBase/SourcesCardBase.test.d.ts +1 -0
- package/dist/cjs/SourcesCardBase/SourcesCardBase.test.js +171 -0
- package/dist/cjs/SourcesCardBase/index.d.ts +2 -0
- package/dist/cjs/SourcesCardBase/index.js +23 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +4 -1
- package/dist/css/main.css +4 -2
- package/dist/css/main.css.map +1 -1
- package/dist/dynamic/SourcesCardBase/package.json +1 -0
- package/dist/esm/Message/Message.d.ts +2 -0
- package/dist/esm/Message/Message.js +6 -2
- package/dist/esm/Message/Message.test.js +17 -2
- package/dist/esm/SourcesCard/SourcesCard.js +4 -31
- package/dist/esm/SourcesCard/SourcesCard.test.js +3 -212
- package/dist/esm/SourcesCardBase/SourcesCardBase.d.ts +57 -0
- package/dist/esm/SourcesCardBase/SourcesCardBase.js +47 -0
- package/dist/esm/SourcesCardBase/SourcesCardBase.test.d.ts +1 -0
- package/dist/esm/SourcesCardBase/SourcesCardBase.test.js +166 -0
- package/dist/esm/SourcesCardBase/index.d.ts +2 -0
- package/dist/esm/SourcesCardBase/index.js +2 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +1 -0
- package/src/Message/Message.test.tsx +25 -2
- package/src/Message/Message.tsx +9 -0
- package/src/SourcesCard/SourcesCard.scss +4 -1
- package/src/SourcesCard/SourcesCard.test.tsx +2 -327
- package/src/SourcesCard/SourcesCard.tsx +8 -171
- package/src/SourcesCardBase/SourcesCardBase.test.tsx +236 -0
- package/src/SourcesCardBase/SourcesCardBase.tsx +242 -0
- package/src/SourcesCardBase/index.ts +3 -0
- package/src/index.ts +3 -0
|
@@ -919,12 +919,16 @@ describe('Message', () => {
|
|
|
919
919
|
expect(screen.getByTestId('after-main-content')).toContainHTML('<strong>Bold after content</strong>');
|
|
920
920
|
expect(screen.getByTestId('end-main-content')).toContainHTML('<strong>Bold end content</strong>');
|
|
921
921
|
});
|
|
922
|
-
it('should handle image correctly', () => {
|
|
922
|
+
it('should handle image correctly for user', () => {
|
|
923
923
|
render(<Message avatar="./img" role="user" name="User" content={IMAGE} />);
|
|
924
|
+
expect(screen.queryByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeFalsy();
|
|
925
|
+
});
|
|
926
|
+
it('should handle image correctly for bot', () => {
|
|
927
|
+
render(<Message avatar="./img" role="bot" name="Bot" content={IMAGE} />);
|
|
924
928
|
expect(screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeTruthy();
|
|
925
929
|
});
|
|
926
930
|
it('inline image parent should have class pf-chatbot__message-and-actions', () => {
|
|
927
|
-
render(<Message avatar="./img" role="
|
|
931
|
+
render(<Message avatar="./img" role="bot" name="Bot" content={INLINE_IMAGE} />);
|
|
928
932
|
expect(screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeTruthy();
|
|
929
933
|
expect(
|
|
930
934
|
screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i }).parentElement
|
|
@@ -1046,6 +1050,25 @@ describe('Message', () => {
|
|
|
1046
1050
|
// code block isn't rendering
|
|
1047
1051
|
expect(screen.queryByRole('button', { name: 'Copy code' })).toBeFalsy();
|
|
1048
1052
|
});
|
|
1053
|
+
it('should disable images and additional tags for user messages', () => {
|
|
1054
|
+
render(
|
|
1055
|
+
<Message
|
|
1056
|
+
avatar="./img"
|
|
1057
|
+
role="user"
|
|
1058
|
+
name="User"
|
|
1059
|
+
content={`${IMAGE} ${CODE_MESSAGE}`}
|
|
1060
|
+
reactMarkdownProps={{ disallowedElements: ['code'] }}
|
|
1061
|
+
/>
|
|
1062
|
+
);
|
|
1063
|
+
expect(screen.getByText('Here is some YAML code:')).toBeTruthy();
|
|
1064
|
+
// code block isn't rendering
|
|
1065
|
+
expect(screen.queryByRole('button', { name: 'Copy code' })).toBeFalsy();
|
|
1066
|
+
expect(screen.queryByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeFalsy();
|
|
1067
|
+
});
|
|
1068
|
+
it('can override image tag removal default for user messages', () => {
|
|
1069
|
+
render(<Message avatar="./img" role="user" name="User" content={IMAGE} hasNoImagesInUserMessages={false} />);
|
|
1070
|
+
expect(screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeTruthy();
|
|
1071
|
+
});
|
|
1049
1072
|
it('should render deep thinking section correctly', () => {
|
|
1050
1073
|
render(<Message avatar="./img" role="user" name="User" content="" deepThinking={DEEP_THINKING} />);
|
|
1051
1074
|
expect(screen.getByRole('button', { name: /Show thinking/i })).toBeTruthy();
|
package/src/Message/Message.tsx
CHANGED
|
@@ -203,6 +203,8 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
|
|
|
203
203
|
remarkGfmProps?: Options;
|
|
204
204
|
/** Props for a tool call message */
|
|
205
205
|
toolCall?: ToolCallProps;
|
|
206
|
+
/** Whether user messages default to stripping out images in markdown */
|
|
207
|
+
hasNoImagesInUserMessages?: boolean;
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
export const MessageBase: FunctionComponent<MessageProps> = ({
|
|
@@ -249,6 +251,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
|
|
|
249
251
|
deepThinking,
|
|
250
252
|
remarkGfmProps,
|
|
251
253
|
toolCall,
|
|
254
|
+
hasNoImagesInUserMessages = true,
|
|
252
255
|
...props
|
|
253
256
|
}: MessageProps) => {
|
|
254
257
|
const [messageText, setMessageText] = useState(content);
|
|
@@ -275,6 +278,11 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
|
|
|
275
278
|
const date = new Date();
|
|
276
279
|
const dateString = timestamp ?? `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
|
|
277
280
|
|
|
281
|
+
const disallowedElements = role === 'user' && hasNoImagesInUserMessages ? ['img'] : [];
|
|
282
|
+
if (reactMarkdownProps && reactMarkdownProps.disallowedElements) {
|
|
283
|
+
disallowedElements.push(...reactMarkdownProps.disallowedElements);
|
|
284
|
+
}
|
|
285
|
+
|
|
278
286
|
const handleMarkdown = () => {
|
|
279
287
|
if (isMarkdownDisabled) {
|
|
280
288
|
return (
|
|
@@ -415,6 +423,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
|
|
|
415
423
|
footnoteLabelProperties: { className: [''] },
|
|
416
424
|
...reactMarkdownProps?.remarkRehypeOptions
|
|
417
425
|
}}
|
|
426
|
+
disallowedElements={disallowedElements}
|
|
418
427
|
>
|
|
419
428
|
{messageText}
|
|
420
429
|
</Markdown>
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
.pf-chatbot__source
|
|
1
|
+
.pf-chatbot__source,
|
|
2
|
+
.pf-chatbot__sources-card-base {
|
|
2
3
|
display: flex;
|
|
3
4
|
flex-direction: column;
|
|
4
5
|
gap: var(--pf-t--global--spacer--sm);
|
|
5
6
|
padding-block-start: var(--pf-t--global--spacer--sm);
|
|
6
7
|
max-width: 22.5rem;
|
|
8
|
+
}
|
|
7
9
|
|
|
10
|
+
.pf-chatbot__sources-card-base {
|
|
8
11
|
a {
|
|
9
12
|
color: var(--pf-t--global--text--color--link--default) !important;
|
|
10
13
|
-webkit-text-decoration: var(--pf-t--global--text-decoration--link--line--default) !important;
|
|
@@ -1,42 +1,15 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import userEvent from '@testing-library/user-event';
|
|
3
2
|
import '@testing-library/jest-dom';
|
|
4
3
|
import SourcesCard from './SourcesCard';
|
|
5
4
|
|
|
6
5
|
describe('SourcesCard', () => {
|
|
7
|
-
it('should render
|
|
8
|
-
render(<SourcesCard sources={[{ link: '' }]} />);
|
|
9
|
-
expect(screen.getByText('1 source')).toBeTruthy();
|
|
10
|
-
expect(screen.getByText('Source 1')).toBeTruthy();
|
|
11
|
-
// no buttons or navigation when there is only 1 source
|
|
12
|
-
expect(screen.queryByRole('button')).toBeFalsy();
|
|
13
|
-
expect(screen.queryByText('1/1')).toBeFalsy();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should render card correctly if one source with a title is passed in', () => {
|
|
6
|
+
it('should render sources correctly if one source is passed in', () => {
|
|
17
7
|
render(<SourcesCard sources={[{ title: 'How to make an apple pie', link: '' }]} />);
|
|
18
8
|
expect(screen.getByText('1 source')).toBeTruthy();
|
|
19
9
|
expect(screen.getByText('How to make an apple pie')).toBeTruthy();
|
|
20
10
|
});
|
|
21
11
|
|
|
22
|
-
it('should render
|
|
23
|
-
render(<SourcesCard sources={[{ link: '', body: 'To make an apple pie, you must first...' }]} />);
|
|
24
|
-
expect(screen.getByText('1 source')).toBeTruthy();
|
|
25
|
-
expect(screen.getByText('To make an apple pie, you must first...')).toBeTruthy();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should render card correctly if one source with a title and body is passed in', () => {
|
|
29
|
-
render(
|
|
30
|
-
<SourcesCard
|
|
31
|
-
sources={[{ title: 'How to make an apple pie', link: '', body: 'To make an apple pie, you must first...' }]}
|
|
32
|
-
/>
|
|
33
|
-
);
|
|
34
|
-
expect(screen.getByText('1 source')).toBeTruthy();
|
|
35
|
-
expect(screen.getByText('How to make an apple pie')).toBeTruthy();
|
|
36
|
-
expect(screen.getByText('To make an apple pie, you must first...')).toBeTruthy();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should render multiple cards correctly', () => {
|
|
12
|
+
it('should render sources correctly when there is more than one', () => {
|
|
40
13
|
render(
|
|
41
14
|
<SourcesCard
|
|
42
15
|
sources={[
|
|
@@ -51,302 +24,4 @@ describe('SourcesCard', () => {
|
|
|
51
24
|
screen.getByRole('button', { name: /Go to previous page/i });
|
|
52
25
|
screen.getByRole('button', { name: /Go to next page/i });
|
|
53
26
|
});
|
|
54
|
-
|
|
55
|
-
it('should navigate between cards correctly', async () => {
|
|
56
|
-
render(
|
|
57
|
-
<SourcesCard
|
|
58
|
-
sources={[
|
|
59
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
60
|
-
{ title: 'How to make cookies', link: '' }
|
|
61
|
-
]}
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
expect(screen.getByText('How to make an apple pie')).toBeTruthy();
|
|
65
|
-
expect(screen.getByText('1/2')).toBeTruthy();
|
|
66
|
-
expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeDisabled();
|
|
67
|
-
await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
|
|
68
|
-
expect(screen.queryByText('How to make an apple pie')).toBeFalsy();
|
|
69
|
-
expect(screen.getByText('How to make cookies')).toBeTruthy();
|
|
70
|
-
expect(screen.getByText('2/2')).toBeTruthy();
|
|
71
|
-
expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeEnabled();
|
|
72
|
-
expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should apply className appropriately', () => {
|
|
76
|
-
render(
|
|
77
|
-
<SourcesCard
|
|
78
|
-
sources={[
|
|
79
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
80
|
-
{ title: 'How to make cookies', link: '' }
|
|
81
|
-
]}
|
|
82
|
-
className="test"
|
|
83
|
-
/>
|
|
84
|
-
);
|
|
85
|
-
const element = screen.getByRole('navigation');
|
|
86
|
-
expect(element).toHaveClass('test');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should disable pagination appropriately', () => {
|
|
90
|
-
render(
|
|
91
|
-
<SourcesCard
|
|
92
|
-
sources={[
|
|
93
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
94
|
-
{ title: 'How to make cookies', link: '' }
|
|
95
|
-
]}
|
|
96
|
-
isDisabled
|
|
97
|
-
/>
|
|
98
|
-
);
|
|
99
|
-
expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeDisabled();
|
|
100
|
-
expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should render navigation aria label appropriately', () => {
|
|
104
|
-
render(
|
|
105
|
-
<SourcesCard
|
|
106
|
-
sources={[
|
|
107
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
108
|
-
{ title: 'How to make cookies', link: '' }
|
|
109
|
-
]}
|
|
110
|
-
/>
|
|
111
|
-
);
|
|
112
|
-
expect(screen.getByRole('navigation', { name: /Pagination/i })).toBeTruthy();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should change paginationAriaLabel appropriately', () => {
|
|
116
|
-
render(
|
|
117
|
-
<SourcesCard
|
|
118
|
-
sources={[
|
|
119
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
120
|
-
{ title: 'How to make cookies', link: '' }
|
|
121
|
-
]}
|
|
122
|
-
paginationAriaLabel="Navegación"
|
|
123
|
-
/>
|
|
124
|
-
);
|
|
125
|
-
expect(screen.getByRole('navigation', { name: /Navegación/i })).toBeTruthy();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should change sourceWord appropriately', () => {
|
|
129
|
-
render(<SourcesCard sources={[{ title: 'How to make an apple pie', link: '' }]} sourceWord={'fuente'} />);
|
|
130
|
-
expect(screen.getByText('1 fuente')).toBeTruthy();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('should sourceWordPlural appropriately', () => {
|
|
134
|
-
render(
|
|
135
|
-
<SourcesCard
|
|
136
|
-
sources={[
|
|
137
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
138
|
-
{ title: 'How to make cookies', link: '' }
|
|
139
|
-
]}
|
|
140
|
-
sourceWordPlural={'fuentes'}
|
|
141
|
-
/>
|
|
142
|
-
);
|
|
143
|
-
expect(screen.getByText('2 fuentes')).toBeTruthy();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should change toNextPageAriaLabel appropriately', () => {
|
|
147
|
-
render(
|
|
148
|
-
<SourcesCard
|
|
149
|
-
sources={[
|
|
150
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
151
|
-
{ title: 'How to make cookies', link: '' }
|
|
152
|
-
]}
|
|
153
|
-
toNextPageAriaLabel="Pase a la siguiente página"
|
|
154
|
-
/>
|
|
155
|
-
);
|
|
156
|
-
expect(screen.getByRole('button', { name: /Pase a la siguiente página/i })).toBeTruthy();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should change toPreviousPageAriaLabel appropriately', () => {
|
|
160
|
-
render(
|
|
161
|
-
<SourcesCard
|
|
162
|
-
sources={[
|
|
163
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
164
|
-
{ title: 'How to make cookies', link: '' }
|
|
165
|
-
]}
|
|
166
|
-
toPreviousPageAriaLabel="Presione para regresar a la página anterior"
|
|
167
|
-
/>
|
|
168
|
-
);
|
|
169
|
-
expect(screen.getByRole('button', { name: /Presione para regresar a la página anterior/i })).toBeTruthy();
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('should call onNextClick appropriately', async () => {
|
|
173
|
-
const spy = jest.fn();
|
|
174
|
-
render(
|
|
175
|
-
<SourcesCard
|
|
176
|
-
sources={[
|
|
177
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
178
|
-
{ title: 'How to make cookies', link: '' }
|
|
179
|
-
]}
|
|
180
|
-
onNextClick={spy}
|
|
181
|
-
/>
|
|
182
|
-
);
|
|
183
|
-
await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
|
|
184
|
-
expect(spy).toHaveBeenCalled();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('should call onPreviousClick appropriately', async () => {
|
|
188
|
-
const spy = jest.fn();
|
|
189
|
-
render(
|
|
190
|
-
<SourcesCard
|
|
191
|
-
sources={[
|
|
192
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
193
|
-
{ title: 'How to make cookies', link: '' }
|
|
194
|
-
]}
|
|
195
|
-
onPreviousClick={spy}
|
|
196
|
-
/>
|
|
197
|
-
);
|
|
198
|
-
await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
|
|
199
|
-
await userEvent.click(screen.getByRole('button', { name: /Go to previous page/i }));
|
|
200
|
-
expect(spy).toHaveBeenCalled();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('should call onSetPage appropriately', async () => {
|
|
204
|
-
const spy = jest.fn();
|
|
205
|
-
render(
|
|
206
|
-
<SourcesCard
|
|
207
|
-
sources={[
|
|
208
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
209
|
-
{ title: 'How to make cookies', link: '' }
|
|
210
|
-
]}
|
|
211
|
-
onSetPage={spy}
|
|
212
|
-
/>
|
|
213
|
-
);
|
|
214
|
-
await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
|
|
215
|
-
expect(spy).toHaveBeenCalledTimes(1);
|
|
216
|
-
await userEvent.click(screen.getByRole('button', { name: /Go to previous page/i }));
|
|
217
|
-
expect(spy).toHaveBeenCalledTimes(2);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('should handle showMore appropriately', async () => {
|
|
221
|
-
render(
|
|
222
|
-
<SourcesCard
|
|
223
|
-
sources={[
|
|
224
|
-
{
|
|
225
|
-
title: 'Getting started with Red Hat OpenShift',
|
|
226
|
-
link: '#',
|
|
227
|
-
body: 'Red Hat OpenShift on IBM Cloud is a managed offering to create your own cluster of compute hosts where you can deploy and manage containerized apps on IBM Cloud ...',
|
|
228
|
-
hasShowMore: true
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
title: 'Azure Red Hat OpenShift documentation',
|
|
232
|
-
link: '#',
|
|
233
|
-
body: 'Microsoft Azure Red Hat OpenShift allows you to deploy a production ready Red Hat OpenShift cluster in Azure ...'
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
title: 'OKD Documentation: Home',
|
|
237
|
-
link: '#',
|
|
238
|
-
body: 'OKD is a distribution of Kubernetes optimized for continuous application development and multi-tenant deployment. OKD also serves as the upstream code base upon ...'
|
|
239
|
-
}
|
|
240
|
-
]}
|
|
241
|
-
/>
|
|
242
|
-
);
|
|
243
|
-
expect(screen.getByRole('region')).toHaveAttribute('class', 'pf-v6-c-expandable-section__content');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('should call onClick appropriately', async () => {
|
|
247
|
-
const spy = jest.fn();
|
|
248
|
-
render(<SourcesCard sources={[{ title: 'How to make an apple pie', link: '', onClick: spy }]} />);
|
|
249
|
-
await userEvent.click(screen.getByRole('link', { name: /How to make an apple pie/i }));
|
|
250
|
-
expect(spy).toHaveBeenCalled();
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should apply titleProps appropriately', () => {
|
|
254
|
-
render(
|
|
255
|
-
<SourcesCard sources={[{ title: 'How to make an apple pie', link: '', titleProps: { className: 'test' } }]} />
|
|
256
|
-
);
|
|
257
|
-
expect(screen.getByRole('link', { name: /How to make an apple pie/i })).toHaveClass('test');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('should apply cardTitleProps appropriately', () => {
|
|
261
|
-
render(
|
|
262
|
-
<SourcesCard
|
|
263
|
-
cardTitleProps={{ 'data-testid': 'card-title', className: 'test' } as any}
|
|
264
|
-
sources={[{ title: 'How to make an apple pie', link: '' }]}
|
|
265
|
-
/>
|
|
266
|
-
);
|
|
267
|
-
expect(screen.getByTestId('card-title')).toHaveClass('test');
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('should apply cardBodyProps appropriately', () => {
|
|
271
|
-
render(
|
|
272
|
-
<SourcesCard
|
|
273
|
-
cardBodyProps={
|
|
274
|
-
{ 'data-testid': 'card-body', body: 'To make an apple pie, you must first...', className: 'test' } as any
|
|
275
|
-
}
|
|
276
|
-
sources={[{ title: 'How to make an apple pie', link: '', body: 'To make an apple pie, you must first...' }]}
|
|
277
|
-
/>
|
|
278
|
-
);
|
|
279
|
-
expect(screen.getByTestId('card-body')).toHaveClass('test');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('should apply cardFooterProps appropriately', () => {
|
|
283
|
-
render(
|
|
284
|
-
<SourcesCard
|
|
285
|
-
cardFooterProps={{ 'data-testid': 'card-footer', className: 'test' } as any}
|
|
286
|
-
sources={[
|
|
287
|
-
{ title: 'How to make an apple pie', link: '' },
|
|
288
|
-
{ title: 'How to make cookies', link: '' }
|
|
289
|
-
]}
|
|
290
|
-
/>
|
|
291
|
-
);
|
|
292
|
-
expect(screen.getByTestId('card-footer')).toHaveClass('test');
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('should apply truncateProps appropriately', () => {
|
|
296
|
-
render(
|
|
297
|
-
<SourcesCard
|
|
298
|
-
sources={[
|
|
299
|
-
{
|
|
300
|
-
title: 'How to make an apple pie',
|
|
301
|
-
link: '',
|
|
302
|
-
truncateProps: { 'data-testid': 'card-truncate', className: 'test' } as any
|
|
303
|
-
}
|
|
304
|
-
]}
|
|
305
|
-
/>
|
|
306
|
-
);
|
|
307
|
-
expect(screen.getByTestId('card-truncate')).toHaveClass('test');
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('should apply custom footer appropriately when there is one source', () => {
|
|
311
|
-
render(
|
|
312
|
-
<SourcesCard sources={[{ title: 'How to make an apple pie', link: '', footer: <>I am a custom footer</> }]} />
|
|
313
|
-
);
|
|
314
|
-
expect(screen.getByText('I am a custom footer'));
|
|
315
|
-
expect(screen.queryByText('1/1')).toBeFalsy();
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('should apply custom footer appropriately when are multiple sources', () => {
|
|
319
|
-
render(
|
|
320
|
-
<SourcesCard
|
|
321
|
-
sources={[
|
|
322
|
-
{ title: 'How to make an apple pie', link: '', footer: <>I am a custom footer</> },
|
|
323
|
-
{ title: 'How to bake bread', link: '' }
|
|
324
|
-
]}
|
|
325
|
-
/>
|
|
326
|
-
);
|
|
327
|
-
expect(screen.getByText('I am a custom footer'));
|
|
328
|
-
// does not show navigation bar
|
|
329
|
-
expect(screen.queryByText('1/2')).toBeFalsy();
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('should apply footer props to custom footer appropriately', () => {
|
|
333
|
-
render(
|
|
334
|
-
<SourcesCard
|
|
335
|
-
cardFooterProps={{ 'data-testid': 'card-footer', className: 'test' } as any}
|
|
336
|
-
sources={[{ title: 'How to make an apple pie', link: '', footer: <>I am a custom footer</> }]}
|
|
337
|
-
/>
|
|
338
|
-
);
|
|
339
|
-
expect(screen.getByText('I am a custom footer'));
|
|
340
|
-
expect(screen.getByTestId('card-footer')).toHaveClass('test');
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it('should apply subtitle appropriately', () => {
|
|
344
|
-
render(
|
|
345
|
-
<SourcesCard
|
|
346
|
-
sources={[{ title: 'How to make an apple pie', link: '', subtitle: 'You must first create the universe' }]}
|
|
347
|
-
/>
|
|
348
|
-
);
|
|
349
|
-
expect(screen.getByText('How to make an apple pie'));
|
|
350
|
-
expect(screen.getByText('You must first create the universe'));
|
|
351
|
-
});
|
|
352
27
|
});
|
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
2
|
// Chatbot Main - Messages - Sources Card
|
|
3
3
|
// ============================================================================
|
|
4
|
-
import type { FunctionComponent
|
|
5
|
-
import { useState } from 'react';
|
|
4
|
+
import type { FunctionComponent } from 'react';
|
|
6
5
|
// Import PatternFly components
|
|
7
6
|
import {
|
|
8
|
-
Button,
|
|
9
7
|
ButtonProps,
|
|
10
|
-
ButtonVariant,
|
|
11
|
-
Card,
|
|
12
|
-
CardBody,
|
|
13
8
|
CardBodyProps,
|
|
14
|
-
CardFooter,
|
|
15
9
|
CardFooterProps,
|
|
16
10
|
CardProps,
|
|
17
|
-
CardTitle,
|
|
18
11
|
CardTitleProps,
|
|
19
|
-
ExpandableSection,
|
|
20
|
-
ExpandableSectionVariant,
|
|
21
|
-
Icon,
|
|
22
12
|
pluralize,
|
|
23
|
-
Truncate,
|
|
24
13
|
TruncateProps
|
|
25
14
|
} from '@patternfly/react-core';
|
|
26
|
-
import
|
|
15
|
+
import SourcesCardBase from '../SourcesCardBase';
|
|
27
16
|
|
|
28
17
|
export interface SourcesCardProps extends CardProps {
|
|
29
18
|
/** Additional classes for the pagination navigation container. */
|
|
@@ -84,167 +73,15 @@ export interface SourcesCardProps extends CardProps {
|
|
|
84
73
|
}
|
|
85
74
|
|
|
86
75
|
const SourcesCard: FunctionComponent<SourcesCardProps> = ({
|
|
87
|
-
className,
|
|
88
|
-
isDisabled,
|
|
89
|
-
paginationAriaLabel = 'Pagination',
|
|
90
76
|
sources,
|
|
91
77
|
sourceWord = 'source',
|
|
92
78
|
sourceWordPlural = 'sources',
|
|
93
|
-
toNextPageAriaLabel = 'Go to next page',
|
|
94
|
-
toPreviousPageAriaLabel = 'Go to previous page',
|
|
95
|
-
onNextClick,
|
|
96
|
-
onPreviousClick,
|
|
97
|
-
onSetPage,
|
|
98
|
-
showMoreWords = 'show more',
|
|
99
|
-
showLessWords = 'show less',
|
|
100
|
-
isCompact,
|
|
101
|
-
cardTitleProps,
|
|
102
|
-
cardBodyProps,
|
|
103
|
-
cardFooterProps,
|
|
104
79
|
...props
|
|
105
|
-
}: SourcesCardProps) =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const handleNewPage = (_evt: ReactMouseEvent | ReactKeyboardEvent | MouseEvent, newPage: number) => {
|
|
114
|
-
setPage(newPage);
|
|
115
|
-
onSetPage && onSetPage(_evt, newPage);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const renderTitle = (title?: string, truncateProps?: TruncateProps) => {
|
|
119
|
-
if (title) {
|
|
120
|
-
return <Truncate content={title} {...truncateProps} />;
|
|
121
|
-
}
|
|
122
|
-
return `Source ${page}`;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<div className="pf-chatbot__source">
|
|
127
|
-
<span>{pluralize(sources.length, sourceWord, sourceWordPlural)}</span>
|
|
128
|
-
<Card isCompact={isCompact} className="pf-chatbot__sources-card" {...props}>
|
|
129
|
-
<CardTitle className="pf-chatbot__sources-card-title" {...cardTitleProps}>
|
|
130
|
-
<div className="pf-chatbot__sources-card-title-container">
|
|
131
|
-
<Button
|
|
132
|
-
component="a"
|
|
133
|
-
variant={ButtonVariant.link}
|
|
134
|
-
href={sources[page - 1].link}
|
|
135
|
-
icon={sources[page - 1].isExternal ? <ExternalLinkSquareAltIcon /> : undefined}
|
|
136
|
-
iconPosition="end"
|
|
137
|
-
isInline
|
|
138
|
-
rel={sources[page - 1].isExternal ? 'noreferrer' : undefined}
|
|
139
|
-
target={sources[page - 1].isExternal ? '_blank' : undefined}
|
|
140
|
-
onClick={sources[page - 1].onClick ?? undefined}
|
|
141
|
-
{...sources[page - 1].titleProps}
|
|
142
|
-
>
|
|
143
|
-
{renderTitle(sources[page - 1].title, sources[page - 1].truncateProps)}
|
|
144
|
-
</Button>
|
|
145
|
-
{sources[page - 1].subtitle && (
|
|
146
|
-
<span className="pf-chatbot__sources-card-subtitle">{sources[page - 1].subtitle}</span>
|
|
147
|
-
)}
|
|
148
|
-
</div>
|
|
149
|
-
</CardTitle>
|
|
150
|
-
{sources[page - 1].body && (
|
|
151
|
-
<CardBody
|
|
152
|
-
className={`pf-chatbot__sources-card-body ${sources[page - 1].footer ? 'pf-chatbot__compact-sources-card-body' : undefined}`}
|
|
153
|
-
{...cardBodyProps}
|
|
154
|
-
>
|
|
155
|
-
{sources[page - 1].hasShowMore ? (
|
|
156
|
-
// prevents extra VO announcements of button text - parent Message has aria-live
|
|
157
|
-
<div aria-live="off">
|
|
158
|
-
<ExpandableSection
|
|
159
|
-
variant={ExpandableSectionVariant.truncate}
|
|
160
|
-
toggleText={isExpanded ? showLessWords : showMoreWords}
|
|
161
|
-
onToggle={onToggle}
|
|
162
|
-
isExpanded={isExpanded}
|
|
163
|
-
truncateMaxLines={2}
|
|
164
|
-
>
|
|
165
|
-
{sources[page - 1].body}
|
|
166
|
-
</ExpandableSection>
|
|
167
|
-
</div>
|
|
168
|
-
) : (
|
|
169
|
-
<div className="pf-chatbot__sources-card-body-text">{sources[page - 1].body}</div>
|
|
170
|
-
)}
|
|
171
|
-
</CardBody>
|
|
172
|
-
)}
|
|
173
|
-
{sources[page - 1].footer ? (
|
|
174
|
-
<CardFooter className="pf-chatbot__sources-card-footer" {...cardFooterProps}>
|
|
175
|
-
{sources[page - 1].footer}
|
|
176
|
-
</CardFooter>
|
|
177
|
-
) : (
|
|
178
|
-
sources.length > 1 && (
|
|
179
|
-
<CardFooter className="pf-chatbot__sources-card-footer-container" {...cardFooterProps}>
|
|
180
|
-
<div className="pf-chatbot__sources-card-footer">
|
|
181
|
-
<nav
|
|
182
|
-
className={`pf-chatbot__sources-card-footer-buttons ${className}`}
|
|
183
|
-
aria-label={paginationAriaLabel}
|
|
184
|
-
>
|
|
185
|
-
<Button
|
|
186
|
-
variant={ButtonVariant.plain}
|
|
187
|
-
isDisabled={isDisabled || page === 1}
|
|
188
|
-
data-action="previous"
|
|
189
|
-
onClick={(event) => {
|
|
190
|
-
const newPage = page >= 1 ? page - 1 : 1;
|
|
191
|
-
onPreviousClick && onPreviousClick(event, newPage);
|
|
192
|
-
handleNewPage(event, newPage);
|
|
193
|
-
}}
|
|
194
|
-
aria-label={toPreviousPageAriaLabel}
|
|
195
|
-
>
|
|
196
|
-
<Icon iconSize="lg">
|
|
197
|
-
{/* these are inline because the viewBox that works in a round icon is different than the PatternFly default */}
|
|
198
|
-
<svg
|
|
199
|
-
className="pf-v6-svg"
|
|
200
|
-
viewBox="0 0 280 500"
|
|
201
|
-
fill="currentColor"
|
|
202
|
-
aria-hidden="true"
|
|
203
|
-
role="img"
|
|
204
|
-
width="1em"
|
|
205
|
-
height="1em"
|
|
206
|
-
>
|
|
207
|
-
<path d="M31.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L127.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L201.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34z"></path>
|
|
208
|
-
</svg>
|
|
209
|
-
</Icon>
|
|
210
|
-
</Button>
|
|
211
|
-
<span aria-hidden="true">
|
|
212
|
-
{page}/{sources.length}
|
|
213
|
-
</span>
|
|
214
|
-
<Button
|
|
215
|
-
variant={ButtonVariant.plain}
|
|
216
|
-
isDisabled={isDisabled || page === sources.length}
|
|
217
|
-
aria-label={toNextPageAriaLabel}
|
|
218
|
-
data-action="next"
|
|
219
|
-
onClick={(event) => {
|
|
220
|
-
const newPage = page + 1 <= sources.length ? page + 1 : sources.length;
|
|
221
|
-
onNextClick && onNextClick(event, newPage);
|
|
222
|
-
handleNewPage(event, newPage);
|
|
223
|
-
}}
|
|
224
|
-
>
|
|
225
|
-
<Icon isInline iconSize="lg">
|
|
226
|
-
{/* these are inline because the viewBox that works in a round icon is different than the PatternFly default */}
|
|
227
|
-
<svg
|
|
228
|
-
className="pf-v6-svg"
|
|
229
|
-
viewBox="0 0 180 500"
|
|
230
|
-
fill="currentColor"
|
|
231
|
-
aria-hidden="true"
|
|
232
|
-
role="img"
|
|
233
|
-
width="1em"
|
|
234
|
-
height="1em"
|
|
235
|
-
>
|
|
236
|
-
<path d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z"></path>
|
|
237
|
-
</svg>
|
|
238
|
-
</Icon>
|
|
239
|
-
</Button>
|
|
240
|
-
</nav>
|
|
241
|
-
</div>
|
|
242
|
-
</CardFooter>
|
|
243
|
-
)
|
|
244
|
-
)}
|
|
245
|
-
</Card>
|
|
246
|
-
</div>
|
|
247
|
-
);
|
|
248
|
-
};
|
|
80
|
+
}: SourcesCardProps) => (
|
|
81
|
+
<div className="pf-chatbot__source">
|
|
82
|
+
<span>{pluralize(sources.length, sourceWord, sourceWordPlural)}</span>
|
|
83
|
+
<SourcesCardBase sources={sources} {...props} />
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
249
86
|
|
|
250
87
|
export default SourcesCard;
|