@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.
Files changed (41) hide show
  1. package/dist/cjs/Message/Message.d.ts +2 -0
  2. package/dist/cjs/Message/Message.js +6 -2
  3. package/dist/cjs/Message/Message.test.js +17 -2
  4. package/dist/cjs/SourcesCard/SourcesCard.js +6 -30
  5. package/dist/cjs/SourcesCard/SourcesCard.test.js +2 -211
  6. package/dist/cjs/SourcesCardBase/SourcesCardBase.d.ts +57 -0
  7. package/dist/cjs/SourcesCardBase/SourcesCardBase.js +49 -0
  8. package/dist/cjs/SourcesCardBase/SourcesCardBase.test.d.ts +1 -0
  9. package/dist/cjs/SourcesCardBase/SourcesCardBase.test.js +171 -0
  10. package/dist/cjs/SourcesCardBase/index.d.ts +2 -0
  11. package/dist/cjs/SourcesCardBase/index.js +23 -0
  12. package/dist/cjs/index.d.ts +2 -0
  13. package/dist/cjs/index.js +4 -1
  14. package/dist/css/main.css +4 -2
  15. package/dist/css/main.css.map +1 -1
  16. package/dist/dynamic/SourcesCardBase/package.json +1 -0
  17. package/dist/esm/Message/Message.d.ts +2 -0
  18. package/dist/esm/Message/Message.js +6 -2
  19. package/dist/esm/Message/Message.test.js +17 -2
  20. package/dist/esm/SourcesCard/SourcesCard.js +4 -31
  21. package/dist/esm/SourcesCard/SourcesCard.test.js +3 -212
  22. package/dist/esm/SourcesCardBase/SourcesCardBase.d.ts +57 -0
  23. package/dist/esm/SourcesCardBase/SourcesCardBase.js +47 -0
  24. package/dist/esm/SourcesCardBase/SourcesCardBase.test.d.ts +1 -0
  25. package/dist/esm/SourcesCardBase/SourcesCardBase.test.js +166 -0
  26. package/dist/esm/SourcesCardBase/index.d.ts +2 -0
  27. package/dist/esm/SourcesCardBase/index.js +2 -0
  28. package/dist/esm/index.d.ts +2 -0
  29. package/dist/esm/index.js +2 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +1 -1
  32. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +1 -0
  33. package/src/Message/Message.test.tsx +25 -2
  34. package/src/Message/Message.tsx +9 -0
  35. package/src/SourcesCard/SourcesCard.scss +4 -1
  36. package/src/SourcesCard/SourcesCard.test.tsx +2 -327
  37. package/src/SourcesCard/SourcesCard.tsx +8 -171
  38. package/src/SourcesCardBase/SourcesCardBase.test.tsx +236 -0
  39. package/src/SourcesCardBase/SourcesCardBase.tsx +242 -0
  40. package/src/SourcesCardBase/index.ts +3 -0
  41. package/src/index.ts +3 -0
@@ -158,6 +158,8 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
158
158
  remarkGfmProps?: Options;
159
159
  /** Props for a tool call message */
160
160
  toolCall?: ToolCallProps;
161
+ /** Whether user messages default to stripping out images in markdown */
162
+ hasNoImagesInUserMessages?: boolean;
161
163
  }
162
164
  export declare const MessageBase: FunctionComponent<MessageProps>;
163
165
  declare const Message: import("react").ForwardRefExoticComponent<Omit<MessageProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -55,7 +55,7 @@ const DeepThinking_1 = __importDefault(require("../DeepThinking"));
55
55
  const SuperscriptMessage_1 = __importDefault(require("./SuperscriptMessage/SuperscriptMessage"));
56
56
  const ToolCall_1 = __importDefault(require("../ToolCall"));
57
57
  const MessageBase = (_a) => {
58
- var { role, content, extraContent, name, avatar, timestamp, isLoading, actions, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps, toolResponse, deepThinking, remarkGfmProps, toolCall } = _a, props = __rest(_a, ["role", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "additionalRemarkPlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps", "toolResponse", "deepThinking", "remarkGfmProps", "toolCall"]);
58
+ var { role, content, extraContent, name, avatar, timestamp, isLoading, actions, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps, toolResponse, deepThinking, remarkGfmProps, toolCall, hasNoImagesInUserMessages = true } = _a, props = __rest(_a, ["role", "content", "extraContent", "name", "avatar", "timestamp", "isLoading", "actions", "sources", "botWord", "loadingWord", "codeBlockProps", "quickResponses", "quickResponseContainerProps", "attachments", "hasRoundAvatar", "avatarProps", "quickStarts", "userFeedbackForm", "userFeedbackComplete", "isLiveRegion", "innerRef", "tableProps", "openLinkInNewTab", "additionalRehypePlugins", "additionalRemarkPlugins", "linkProps", "error", "isEditable", "editPlaceholder", "updateWord", "cancelWord", "onEditUpdate", "onEditCancel", "inputRef", "editFormProps", "isCompact", "isMarkdownDisabled", "reactMarkdownProps", "toolResponse", "deepThinking", "remarkGfmProps", "toolCall", "hasNoImagesInUserMessages"]);
59
59
  const [messageText, setMessageText] = (0, react_1.useState)(content);
60
60
  (0, react_1.useEffect)(() => {
61
61
  setMessageText(content);
@@ -77,6 +77,10 @@ const MessageBase = (_a) => {
77
77
  // Keep timestamps consistent between Timestamp component and aria-label
78
78
  const date = new Date();
79
79
  const dateString = timestamp !== null && timestamp !== void 0 ? timestamp : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
80
+ const disallowedElements = role === 'user' && hasNoImagesInUserMessages ? ['img'] : [];
81
+ if (reactMarkdownProps && reactMarkdownProps.disallowedElements) {
82
+ disallowedElements.push(...reactMarkdownProps.disallowedElements);
83
+ }
80
84
  const handleMarkdown = () => {
81
85
  if (isMarkdownDisabled) {
82
86
  return ((0, jsx_runtime_1.jsx)(TextMessage_1.default, Object.assign({ component: react_core_1.ContentVariants.p }, props, { children: messageText })));
@@ -198,7 +202,7 @@ const MessageBase = (_a) => {
198
202
  }
199
203
  }, remarkPlugins: [[remark_gfm_1.default, Object.assign({}, remarkGfmProps)], ...additionalRemarkPlugins], rehypePlugins: rehypePlugins }, reactMarkdownProps, { remarkRehypeOptions: Object.assign({
200
204
  // removes sr-only class from footnote labels applied by default
201
- footnoteLabelProperties: { className: [''] } }, reactMarkdownProps === null || reactMarkdownProps === void 0 ? void 0 : reactMarkdownProps.remarkRehypeOptions), children: messageText })));
205
+ footnoteLabelProperties: { className: [''] } }, reactMarkdownProps === null || reactMarkdownProps === void 0 ? void 0 : reactMarkdownProps.remarkRehypeOptions), disallowedElements: disallowedElements, children: messageText })));
202
206
  };
203
207
  const renderMessage = () => {
204
208
  if (isLoading) {
@@ -705,12 +705,16 @@ describe('Message', () => {
705
705
  expect(react_2.screen.getByTestId('after-main-content')).toContainHTML('<strong>Bold after content</strong>');
706
706
  expect(react_2.screen.getByTestId('end-main-content')).toContainHTML('<strong>Bold end content</strong>');
707
707
  });
708
- it('should handle image correctly', () => {
708
+ it('should handle image correctly for user', () => {
709
709
  (0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { avatar: "./img", role: "user", name: "User", content: IMAGE }));
710
+ expect(react_2.screen.queryByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeFalsy();
711
+ });
712
+ it('should handle image correctly for bot', () => {
713
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { avatar: "./img", role: "bot", name: "Bot", content: IMAGE }));
710
714
  expect(react_2.screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeTruthy();
711
715
  });
712
716
  it('inline image parent should have class pf-chatbot__message-and-actions', () => {
713
- (0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { avatar: "./img", role: "user", name: "User", content: INLINE_IMAGE }));
717
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { avatar: "./img", role: "bot", name: "Bot", content: INLINE_IMAGE }));
714
718
  expect(react_2.screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeTruthy();
715
719
  expect(react_2.screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i }).parentElement).toHaveClass('pf-chatbot__message-and-actions');
716
720
  });
@@ -804,6 +808,17 @@ describe('Message', () => {
804
808
  // code block isn't rendering
805
809
  expect(react_2.screen.queryByRole('button', { name: 'Copy code' })).toBeFalsy();
806
810
  });
811
+ it('should disable images and additional tags for user messages', () => {
812
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { avatar: "./img", role: "user", name: "User", content: `${IMAGE} ${CODE_MESSAGE}`, reactMarkdownProps: { disallowedElements: ['code'] } }));
813
+ expect(react_2.screen.getByText('Here is some YAML code:')).toBeTruthy();
814
+ // code block isn't rendering
815
+ expect(react_2.screen.queryByRole('button', { name: 'Copy code' })).toBeFalsy();
816
+ expect(react_2.screen.queryByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeFalsy();
817
+ });
818
+ it('can override image tag removal default for user messages', () => {
819
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { avatar: "./img", role: "user", name: "User", content: IMAGE, hasNoImagesInUserMessages: false }));
820
+ expect(react_2.screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeTruthy();
821
+ });
807
822
  it('should render deep thinking section correctly', () => {
808
823
  (0, react_2.render)((0, jsx_runtime_1.jsx)(Message_1.default, { avatar: "./img", role: "user", name: "User", content: "", deepThinking: DEEP_THINKING }));
809
824
  expect(react_2.screen.getByRole('button', { name: /Show thinking/i })).toBeTruthy();
@@ -10,40 +10,16 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  }
11
11
  return t;
12
12
  };
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
13
16
  Object.defineProperty(exports, "__esModule", { value: true });
14
17
  const jsx_runtime_1 = require("react/jsx-runtime");
15
- const react_1 = require("react");
16
18
  // Import PatternFly components
17
19
  const react_core_1 = require("@patternfly/react-core");
18
- const react_icons_1 = require("@patternfly/react-icons");
20
+ const SourcesCardBase_1 = __importDefault(require("../SourcesCardBase"));
19
21
  const SourcesCard = (_a) => {
20
- var _b;
21
- var { className, isDisabled, paginationAriaLabel = 'Pagination', sources, sourceWord = 'source', sourceWordPlural = 'sources', toNextPageAriaLabel = 'Go to next page', toPreviousPageAriaLabel = 'Go to previous page', onNextClick, onPreviousClick, onSetPage, showMoreWords = 'show more', showLessWords = 'show less', isCompact, cardTitleProps, cardBodyProps, cardFooterProps } = _a, props = __rest(_a, ["className", "isDisabled", "paginationAriaLabel", "sources", "sourceWord", "sourceWordPlural", "toNextPageAriaLabel", "toPreviousPageAriaLabel", "onNextClick", "onPreviousClick", "onSetPage", "showMoreWords", "showLessWords", "isCompact", "cardTitleProps", "cardBodyProps", "cardFooterProps"]);
22
- const [page, setPage] = (0, react_1.useState)(1);
23
- const [isExpanded, setIsExpanded] = (0, react_1.useState)(false);
24
- const onToggle = (_event, isExpanded) => {
25
- setIsExpanded(isExpanded);
26
- };
27
- const handleNewPage = (_evt, newPage) => {
28
- setPage(newPage);
29
- onSetPage && onSetPage(_evt, newPage);
30
- };
31
- const renderTitle = (title, truncateProps) => {
32
- if (title) {
33
- return (0, jsx_runtime_1.jsx)(react_core_1.Truncate, Object.assign({ content: title }, truncateProps));
34
- }
35
- return `Source ${page}`;
36
- };
37
- return ((0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__source", children: [(0, jsx_runtime_1.jsx)("span", { children: (0, react_core_1.pluralize)(sources.length, sourceWord, sourceWordPlural) }), (0, jsx_runtime_1.jsxs)(react_core_1.Card, Object.assign({ isCompact: isCompact, className: "pf-chatbot__sources-card" }, props, { children: [(0, jsx_runtime_1.jsx)(react_core_1.CardTitle, Object.assign({ className: "pf-chatbot__sources-card-title" }, cardTitleProps, { children: (0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__sources-card-title-container", children: [(0, jsx_runtime_1.jsx)(react_core_1.Button, Object.assign({ component: "a", variant: react_core_1.ButtonVariant.link, href: sources[page - 1].link, icon: sources[page - 1].isExternal ? (0, jsx_runtime_1.jsx)(react_icons_1.ExternalLinkSquareAltIcon, {}) : undefined, iconPosition: "end", isInline: true, rel: sources[page - 1].isExternal ? 'noreferrer' : undefined, target: sources[page - 1].isExternal ? '_blank' : undefined, onClick: (_b = sources[page - 1].onClick) !== null && _b !== void 0 ? _b : undefined }, sources[page - 1].titleProps, { children: renderTitle(sources[page - 1].title, sources[page - 1].truncateProps) })), sources[page - 1].subtitle && ((0, jsx_runtime_1.jsx)("span", { className: "pf-chatbot__sources-card-subtitle", children: sources[page - 1].subtitle }))] }) })), sources[page - 1].body && ((0, jsx_runtime_1.jsx)(react_core_1.CardBody, Object.assign({ className: `pf-chatbot__sources-card-body ${sources[page - 1].footer ? 'pf-chatbot__compact-sources-card-body' : undefined}` }, cardBodyProps, { children: sources[page - 1].hasShowMore ? (
38
- // prevents extra VO announcements of button text - parent Message has aria-live
39
- (0, jsx_runtime_1.jsx)("div", { "aria-live": "off", children: (0, jsx_runtime_1.jsx)(react_core_1.ExpandableSection, { variant: react_core_1.ExpandableSectionVariant.truncate, toggleText: isExpanded ? showLessWords : showMoreWords, onToggle: onToggle, isExpanded: isExpanded, truncateMaxLines: 2, children: sources[page - 1].body }) })) : ((0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__sources-card-body-text", children: sources[page - 1].body })) }))), sources[page - 1].footer ? ((0, jsx_runtime_1.jsx)(react_core_1.CardFooter, Object.assign({ className: "pf-chatbot__sources-card-footer" }, cardFooterProps, { children: sources[page - 1].footer }))) : (sources.length > 1 && ((0, jsx_runtime_1.jsx)(react_core_1.CardFooter, Object.assign({ className: "pf-chatbot__sources-card-footer-container" }, cardFooterProps, { children: (0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__sources-card-footer", children: (0, jsx_runtime_1.jsxs)("nav", { className: `pf-chatbot__sources-card-footer-buttons ${className}`, "aria-label": paginationAriaLabel, children: [(0, jsx_runtime_1.jsx)(react_core_1.Button, { variant: react_core_1.ButtonVariant.plain, isDisabled: isDisabled || page === 1, "data-action": "previous", onClick: (event) => {
40
- const newPage = page >= 1 ? page - 1 : 1;
41
- onPreviousClick && onPreviousClick(event, newPage);
42
- handleNewPage(event, newPage);
43
- }, "aria-label": toPreviousPageAriaLabel, children: (0, jsx_runtime_1.jsx)(react_core_1.Icon, { iconSize: "lg", children: (0, jsx_runtime_1.jsx)("svg", { className: "pf-v6-svg", viewBox: "0 0 280 500", fill: "currentColor", "aria-hidden": "true", role: "img", width: "1em", height: "1em", children: (0, jsx_runtime_1.jsx)("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" }) }) }) }), (0, jsx_runtime_1.jsxs)("span", { "aria-hidden": "true", children: [page, "/", sources.length] }), (0, jsx_runtime_1.jsx)(react_core_1.Button, { variant: react_core_1.ButtonVariant.plain, isDisabled: isDisabled || page === sources.length, "aria-label": toNextPageAriaLabel, "data-action": "next", onClick: (event) => {
44
- const newPage = page + 1 <= sources.length ? page + 1 : sources.length;
45
- onNextClick && onNextClick(event, newPage);
46
- handleNewPage(event, newPage);
47
- }, children: (0, jsx_runtime_1.jsx)(react_core_1.Icon, { isInline: true, iconSize: "lg", children: (0, jsx_runtime_1.jsx)("svg", { className: "pf-v6-svg", viewBox: "0 0 180 500", fill: "currentColor", "aria-hidden": "true", role: "img", width: "1em", height: "1em", children: (0, jsx_runtime_1.jsx)("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" }) }) }) })] }) }) }))))] }))] }));
22
+ var { sources, sourceWord = 'source', sourceWordPlural = 'sources' } = _a, props = __rest(_a, ["sources", "sourceWord", "sourceWordPlural"]);
23
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__source", children: [(0, jsx_runtime_1.jsx)("span", { children: (0, react_core_1.pluralize)(sources.length, sourceWord, sourceWordPlural) }), (0, jsx_runtime_1.jsx)(SourcesCardBase_1.default, Object.assign({ sources: sources }, props))] }));
48
24
  };
49
25
  exports.default = SourcesCard;
@@ -1,48 +1,19 @@
1
1
  "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
12
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
4
  };
14
5
  Object.defineProperty(exports, "__esModule", { value: true });
15
6
  const jsx_runtime_1 = require("react/jsx-runtime");
16
7
  const react_1 = require("@testing-library/react");
17
- const user_event_1 = __importDefault(require("@testing-library/user-event"));
18
8
  require("@testing-library/jest-dom");
19
9
  const SourcesCard_1 = __importDefault(require("./SourcesCard"));
20
10
  describe('SourcesCard', () => {
21
- it('should render card correctly if one source with only a link is passed in', () => {
22
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [{ link: '' }] }));
23
- expect(react_1.screen.getByText('1 source')).toBeTruthy();
24
- expect(react_1.screen.getByText('Source 1')).toBeTruthy();
25
- // no buttons or navigation when there is only 1 source
26
- expect(react_1.screen.queryByRole('button')).toBeFalsy();
27
- expect(react_1.screen.queryByText('1/1')).toBeFalsy();
28
- });
29
- it('should render card correctly if one source with a title is passed in', () => {
11
+ it('should render sources correctly if one source is passed in', () => {
30
12
  (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [{ title: 'How to make an apple pie', link: '' }] }));
31
13
  expect(react_1.screen.getByText('1 source')).toBeTruthy();
32
14
  expect(react_1.screen.getByText('How to make an apple pie')).toBeTruthy();
33
15
  });
34
- it('should render card correctly if one source with a body is passed in', () => {
35
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [{ link: '', body: 'To make an apple pie, you must first...' }] }));
36
- expect(react_1.screen.getByText('1 source')).toBeTruthy();
37
- expect(react_1.screen.getByText('To make an apple pie, you must first...')).toBeTruthy();
38
- });
39
- it('should render card correctly if one source with a title and body is passed in', () => {
40
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [{ title: 'How to make an apple pie', link: '', body: 'To make an apple pie, you must first...' }] }));
41
- expect(react_1.screen.getByText('1 source')).toBeTruthy();
42
- expect(react_1.screen.getByText('How to make an apple pie')).toBeTruthy();
43
- expect(react_1.screen.getByText('To make an apple pie, you must first...')).toBeTruthy();
44
- });
45
- it('should render multiple cards correctly', () => {
16
+ it('should render sources correctly when there is more than one', () => {
46
17
  (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
47
18
  { title: 'How to make an apple pie', link: '' },
48
19
  { title: 'How to make cookies', link: '' }
@@ -53,184 +24,4 @@ describe('SourcesCard', () => {
53
24
  react_1.screen.getByRole('button', { name: /Go to previous page/i });
54
25
  react_1.screen.getByRole('button', { name: /Go to next page/i });
55
26
  });
56
- it('should navigate between cards correctly', () => __awaiter(void 0, void 0, void 0, function* () {
57
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
58
- { title: 'How to make an apple pie', link: '' },
59
- { title: 'How to make cookies', link: '' }
60
- ] }));
61
- expect(react_1.screen.getByText('How to make an apple pie')).toBeTruthy();
62
- expect(react_1.screen.getByText('1/2')).toBeTruthy();
63
- expect(react_1.screen.getByRole('button', { name: /Go to previous page/i })).toBeDisabled();
64
- yield user_event_1.default.click(react_1.screen.getByRole('button', { name: /Go to next page/i }));
65
- expect(react_1.screen.queryByText('How to make an apple pie')).toBeFalsy();
66
- expect(react_1.screen.getByText('How to make cookies')).toBeTruthy();
67
- expect(react_1.screen.getByText('2/2')).toBeTruthy();
68
- expect(react_1.screen.getByRole('button', { name: /Go to previous page/i })).toBeEnabled();
69
- expect(react_1.screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
70
- }));
71
- it('should apply className appropriately', () => {
72
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
73
- { title: 'How to make an apple pie', link: '' },
74
- { title: 'How to make cookies', link: '' }
75
- ], className: "test" }));
76
- const element = react_1.screen.getByRole('navigation');
77
- expect(element).toHaveClass('test');
78
- });
79
- it('should disable pagination appropriately', () => {
80
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
81
- { title: 'How to make an apple pie', link: '' },
82
- { title: 'How to make cookies', link: '' }
83
- ], isDisabled: true }));
84
- expect(react_1.screen.getByRole('button', { name: /Go to previous page/i })).toBeDisabled();
85
- expect(react_1.screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
86
- });
87
- it('should render navigation aria label appropriately', () => {
88
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
89
- { title: 'How to make an apple pie', link: '' },
90
- { title: 'How to make cookies', link: '' }
91
- ] }));
92
- expect(react_1.screen.getByRole('navigation', { name: /Pagination/i })).toBeTruthy();
93
- });
94
- it('should change paginationAriaLabel appropriately', () => {
95
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
96
- { title: 'How to make an apple pie', link: '' },
97
- { title: 'How to make cookies', link: '' }
98
- ], paginationAriaLabel: "Navegaci\u00F3n" }));
99
- expect(react_1.screen.getByRole('navigation', { name: /Navegación/i })).toBeTruthy();
100
- });
101
- it('should change sourceWord appropriately', () => {
102
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [{ title: 'How to make an apple pie', link: '' }], sourceWord: 'fuente' }));
103
- expect(react_1.screen.getByText('1 fuente')).toBeTruthy();
104
- });
105
- it('should sourceWordPlural appropriately', () => {
106
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
107
- { title: 'How to make an apple pie', link: '' },
108
- { title: 'How to make cookies', link: '' }
109
- ], sourceWordPlural: 'fuentes' }));
110
- expect(react_1.screen.getByText('2 fuentes')).toBeTruthy();
111
- });
112
- it('should change toNextPageAriaLabel appropriately', () => {
113
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
114
- { title: 'How to make an apple pie', link: '' },
115
- { title: 'How to make cookies', link: '' }
116
- ], toNextPageAriaLabel: "Pase a la siguiente p\u00E1gina" }));
117
- expect(react_1.screen.getByRole('button', { name: /Pase a la siguiente página/i })).toBeTruthy();
118
- });
119
- it('should change toPreviousPageAriaLabel appropriately', () => {
120
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
121
- { title: 'How to make an apple pie', link: '' },
122
- { title: 'How to make cookies', link: '' }
123
- ], toPreviousPageAriaLabel: "Presione para regresar a la p\u00E1gina anterior" }));
124
- expect(react_1.screen.getByRole('button', { name: /Presione para regresar a la página anterior/i })).toBeTruthy();
125
- });
126
- it('should call onNextClick appropriately', () => __awaiter(void 0, void 0, void 0, function* () {
127
- const spy = jest.fn();
128
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
129
- { title: 'How to make an apple pie', link: '' },
130
- { title: 'How to make cookies', link: '' }
131
- ], onNextClick: spy }));
132
- yield user_event_1.default.click(react_1.screen.getByRole('button', { name: /Go to next page/i }));
133
- expect(spy).toHaveBeenCalled();
134
- }));
135
- it('should call onPreviousClick appropriately', () => __awaiter(void 0, void 0, void 0, function* () {
136
- const spy = jest.fn();
137
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
138
- { title: 'How to make an apple pie', link: '' },
139
- { title: 'How to make cookies', link: '' }
140
- ], onPreviousClick: spy }));
141
- yield user_event_1.default.click(react_1.screen.getByRole('button', { name: /Go to next page/i }));
142
- yield user_event_1.default.click(react_1.screen.getByRole('button', { name: /Go to previous page/i }));
143
- expect(spy).toHaveBeenCalled();
144
- }));
145
- it('should call onSetPage appropriately', () => __awaiter(void 0, void 0, void 0, function* () {
146
- const spy = jest.fn();
147
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
148
- { title: 'How to make an apple pie', link: '' },
149
- { title: 'How to make cookies', link: '' }
150
- ], onSetPage: spy }));
151
- yield user_event_1.default.click(react_1.screen.getByRole('button', { name: /Go to next page/i }));
152
- expect(spy).toHaveBeenCalledTimes(1);
153
- yield user_event_1.default.click(react_1.screen.getByRole('button', { name: /Go to previous page/i }));
154
- expect(spy).toHaveBeenCalledTimes(2);
155
- }));
156
- it('should handle showMore appropriately', () => __awaiter(void 0, void 0, void 0, function* () {
157
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
158
- {
159
- title: 'Getting started with Red Hat OpenShift',
160
- link: '#',
161
- 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 ...',
162
- hasShowMore: true
163
- },
164
- {
165
- title: 'Azure Red Hat OpenShift documentation',
166
- link: '#',
167
- body: 'Microsoft Azure Red Hat OpenShift allows you to deploy a production ready Red Hat OpenShift cluster in Azure ...'
168
- },
169
- {
170
- title: 'OKD Documentation: Home',
171
- link: '#',
172
- 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 ...'
173
- }
174
- ] }));
175
- expect(react_1.screen.getByRole('region')).toHaveAttribute('class', 'pf-v6-c-expandable-section__content');
176
- }));
177
- it('should call onClick appropriately', () => __awaiter(void 0, void 0, void 0, function* () {
178
- const spy = jest.fn();
179
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [{ title: 'How to make an apple pie', link: '', onClick: spy }] }));
180
- yield user_event_1.default.click(react_1.screen.getByRole('link', { name: /How to make an apple pie/i }));
181
- expect(spy).toHaveBeenCalled();
182
- }));
183
- it('should apply titleProps appropriately', () => {
184
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [{ title: 'How to make an apple pie', link: '', titleProps: { className: 'test' } }] }));
185
- expect(react_1.screen.getByRole('link', { name: /How to make an apple pie/i })).toHaveClass('test');
186
- });
187
- it('should apply cardTitleProps appropriately', () => {
188
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { cardTitleProps: { 'data-testid': 'card-title', className: 'test' }, sources: [{ title: 'How to make an apple pie', link: '' }] }));
189
- expect(react_1.screen.getByTestId('card-title')).toHaveClass('test');
190
- });
191
- it('should apply cardBodyProps appropriately', () => {
192
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { cardBodyProps: { 'data-testid': 'card-body', body: 'To make an apple pie, you must first...', className: 'test' }, sources: [{ title: 'How to make an apple pie', link: '', body: 'To make an apple pie, you must first...' }] }));
193
- expect(react_1.screen.getByTestId('card-body')).toHaveClass('test');
194
- });
195
- it('should apply cardFooterProps appropriately', () => {
196
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { cardFooterProps: { 'data-testid': 'card-footer', className: 'test' }, sources: [
197
- { title: 'How to make an apple pie', link: '' },
198
- { title: 'How to make cookies', link: '' }
199
- ] }));
200
- expect(react_1.screen.getByTestId('card-footer')).toHaveClass('test');
201
- });
202
- it('should apply truncateProps appropriately', () => {
203
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
204
- {
205
- title: 'How to make an apple pie',
206
- link: '',
207
- truncateProps: { 'data-testid': 'card-truncate', className: 'test' }
208
- }
209
- ] }));
210
- expect(react_1.screen.getByTestId('card-truncate')).toHaveClass('test');
211
- });
212
- it('should apply custom footer appropriately when there is one source', () => {
213
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [{ title: 'How to make an apple pie', link: '', footer: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: "I am a custom footer" }) }] }));
214
- expect(react_1.screen.getByText('I am a custom footer'));
215
- expect(react_1.screen.queryByText('1/1')).toBeFalsy();
216
- });
217
- it('should apply custom footer appropriately when are multiple sources', () => {
218
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [
219
- { title: 'How to make an apple pie', link: '', footer: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: "I am a custom footer" }) },
220
- { title: 'How to bake bread', link: '' }
221
- ] }));
222
- expect(react_1.screen.getByText('I am a custom footer'));
223
- // does not show navigation bar
224
- expect(react_1.screen.queryByText('1/2')).toBeFalsy();
225
- });
226
- it('should apply footer props to custom footer appropriately', () => {
227
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { cardFooterProps: { 'data-testid': 'card-footer', className: 'test' }, sources: [{ title: 'How to make an apple pie', link: '', footer: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: "I am a custom footer" }) }] }));
228
- expect(react_1.screen.getByText('I am a custom footer'));
229
- expect(react_1.screen.getByTestId('card-footer')).toHaveClass('test');
230
- });
231
- it('should apply subtitle appropriately', () => {
232
- (0, react_1.render)((0, jsx_runtime_1.jsx)(SourcesCard_1.default, { sources: [{ title: 'How to make an apple pie', link: '', subtitle: 'You must first create the universe' }] }));
233
- expect(react_1.screen.getByText('How to make an apple pie'));
234
- expect(react_1.screen.getByText('You must first create the universe'));
235
- });
236
27
  });
@@ -0,0 +1,57 @@
1
+ import type { FunctionComponent } from 'react';
2
+ import { ButtonProps, CardBodyProps, CardFooterProps, CardProps, CardTitleProps, TruncateProps } from '@patternfly/react-core';
3
+ export interface SourcesCardBaseProps extends CardProps {
4
+ /** Additional classes for the pagination navigation container. */
5
+ className?: string;
6
+ /** Flag indicating if the pagination is disabled. */
7
+ isDisabled?: boolean;
8
+ /** @deprecated ofWord has been deprecated. Label for the English word "of." */
9
+ ofWord?: string;
10
+ /** Accessible label for the pagination component. */
11
+ paginationAriaLabel?: string;
12
+ /** Content rendered inside the paginated card */
13
+ sources: {
14
+ /** Title of sources card */
15
+ title?: string;
16
+ /** Subtitle of sources card */
17
+ subtitle?: string;
18
+ /** Link to source */
19
+ link: string;
20
+ /** Body of sources card */
21
+ body?: React.ReactNode | string;
22
+ /** Whether link is external */
23
+ isExternal?: boolean;
24
+ /** Whether sources card is expandable */
25
+ hasShowMore?: boolean;
26
+ /** onClick event applied to the title of the Sources card */
27
+ onClick?: React.MouseEventHandler<HTMLButtonElement>;
28
+ /** Any additional props applied to the title of the Sources card */
29
+ titleProps?: ButtonProps;
30
+ /** Custom footer applied to the Sources card */
31
+ footer?: React.ReactNode;
32
+ /** Additional props passed to Truncate component */
33
+ truncateProps?: TruncateProps;
34
+ }[];
35
+ /** Accessible label for the button which moves to the next page. */
36
+ toNextPageAriaLabel?: string;
37
+ /** Accessible label for the button which moves to the previous page. */
38
+ toPreviousPageAriaLabel?: string;
39
+ /** Function called when user clicks to navigate to next page. */
40
+ onNextClick?: (event: React.SyntheticEvent<HTMLButtonElement>, page: number) => void;
41
+ /** Function called when user clicks to navigate to previous page. */
42
+ onPreviousClick?: (event: React.SyntheticEvent<HTMLButtonElement>, page: number) => void;
43
+ /** Function called when page is changed. */
44
+ onSetPage?: (event: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => void;
45
+ /** Label for English words "show more" */
46
+ showMoreWords?: string;
47
+ /** Label for English words "show less" */
48
+ showLessWords?: string;
49
+ /** Additional props passed to card title */
50
+ cardTitleProps?: CardTitleProps;
51
+ /** Additional props passed to card body */
52
+ cardBodyProps?: CardBodyProps;
53
+ /** Additional props passed to card footer */
54
+ cardFooterProps?: CardFooterProps;
55
+ }
56
+ declare const SourcesCardBase: FunctionComponent<SourcesCardBaseProps>;
57
+ export default SourcesCardBase;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const jsx_runtime_1 = require("react/jsx-runtime");
15
+ const react_1 = require("react");
16
+ // Import PatternFly components
17
+ const react_core_1 = require("@patternfly/react-core");
18
+ const react_icons_1 = require("@patternfly/react-icons");
19
+ const SourcesCardBase = (_a) => {
20
+ var _b;
21
+ var { className, isDisabled, paginationAriaLabel = 'Pagination', sources, toNextPageAriaLabel = 'Go to next page', toPreviousPageAriaLabel = 'Go to previous page', onNextClick, onPreviousClick, onSetPage, showMoreWords = 'show more', showLessWords = 'show less', isCompact, cardTitleProps, cardBodyProps, cardFooterProps } = _a, props = __rest(_a, ["className", "isDisabled", "paginationAriaLabel", "sources", "toNextPageAriaLabel", "toPreviousPageAriaLabel", "onNextClick", "onPreviousClick", "onSetPage", "showMoreWords", "showLessWords", "isCompact", "cardTitleProps", "cardBodyProps", "cardFooterProps"]);
22
+ const [page, setPage] = (0, react_1.useState)(1);
23
+ const [isExpanded, setIsExpanded] = (0, react_1.useState)(false);
24
+ const onToggle = (_event, isExpanded) => {
25
+ setIsExpanded(isExpanded);
26
+ };
27
+ const handleNewPage = (_evt, newPage) => {
28
+ setPage(newPage);
29
+ onSetPage && onSetPage(_evt, newPage);
30
+ };
31
+ const renderTitle = (title, truncateProps) => {
32
+ if (title) {
33
+ return (0, jsx_runtime_1.jsx)(react_core_1.Truncate, Object.assign({ content: title }, truncateProps));
34
+ }
35
+ return `Source ${page}`;
36
+ };
37
+ return ((0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__sources-card-base", children: (0, jsx_runtime_1.jsxs)(react_core_1.Card, Object.assign({ isCompact: isCompact, className: "pf-chatbot__sources-card" }, props, { children: [(0, jsx_runtime_1.jsx)(react_core_1.CardTitle, Object.assign({ className: "pf-chatbot__sources-card-title" }, cardTitleProps, { children: (0, jsx_runtime_1.jsxs)("div", { className: "pf-chatbot__sources-card-title-container", children: [(0, jsx_runtime_1.jsx)(react_core_1.Button, Object.assign({ component: "a", variant: react_core_1.ButtonVariant.link, href: sources[page - 1].link, icon: sources[page - 1].isExternal ? (0, jsx_runtime_1.jsx)(react_icons_1.ExternalLinkSquareAltIcon, {}) : undefined, iconPosition: "end", isInline: true, rel: sources[page - 1].isExternal ? 'noreferrer' : undefined, target: sources[page - 1].isExternal ? '_blank' : undefined, onClick: (_b = sources[page - 1].onClick) !== null && _b !== void 0 ? _b : undefined }, sources[page - 1].titleProps, { children: renderTitle(sources[page - 1].title, sources[page - 1].truncateProps) })), sources[page - 1].subtitle && ((0, jsx_runtime_1.jsx)("span", { className: "pf-chatbot__sources-card-subtitle", children: sources[page - 1].subtitle }))] }) })), sources[page - 1].body && ((0, jsx_runtime_1.jsx)(react_core_1.CardBody, Object.assign({ className: `pf-chatbot__sources-card-body ${sources[page - 1].footer ? 'pf-chatbot__compact-sources-card-body' : undefined}` }, cardBodyProps, { children: sources[page - 1].hasShowMore ? (
38
+ // prevents extra VO announcements of button text - parent Message has aria-live
39
+ (0, jsx_runtime_1.jsx)("div", { "aria-live": "off", children: (0, jsx_runtime_1.jsx)(react_core_1.ExpandableSection, { variant: react_core_1.ExpandableSectionVariant.truncate, toggleText: isExpanded ? showLessWords : showMoreWords, onToggle: onToggle, isExpanded: isExpanded, truncateMaxLines: 2, children: sources[page - 1].body }) })) : ((0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__sources-card-body-text", children: sources[page - 1].body })) }))), sources[page - 1].footer ? ((0, jsx_runtime_1.jsx)(react_core_1.CardFooter, Object.assign({ className: "pf-chatbot__sources-card-footer" }, cardFooterProps, { children: sources[page - 1].footer }))) : (sources.length > 1 && ((0, jsx_runtime_1.jsx)(react_core_1.CardFooter, Object.assign({ className: "pf-chatbot__sources-card-footer-container" }, cardFooterProps, { children: (0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__sources-card-footer", children: (0, jsx_runtime_1.jsxs)("nav", { className: `pf-chatbot__sources-card-footer-buttons ${className}`, "aria-label": paginationAriaLabel, children: [(0, jsx_runtime_1.jsx)(react_core_1.Button, { variant: react_core_1.ButtonVariant.plain, isDisabled: isDisabled || page === 1, "data-action": "previous", onClick: (event) => {
40
+ const newPage = page >= 1 ? page - 1 : 1;
41
+ onPreviousClick && onPreviousClick(event, newPage);
42
+ handleNewPage(event, newPage);
43
+ }, "aria-label": toPreviousPageAriaLabel, children: (0, jsx_runtime_1.jsx)(react_core_1.Icon, { iconSize: "lg", children: (0, jsx_runtime_1.jsx)("svg", { className: "pf-v6-svg", viewBox: "0 0 280 500", fill: "currentColor", "aria-hidden": "true", role: "img", width: "1em", height: "1em", children: (0, jsx_runtime_1.jsx)("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" }) }) }) }), (0, jsx_runtime_1.jsxs)("span", { "aria-hidden": "true", children: [page, "/", sources.length] }), (0, jsx_runtime_1.jsx)(react_core_1.Button, { variant: react_core_1.ButtonVariant.plain, isDisabled: isDisabled || page === sources.length, "aria-label": toNextPageAriaLabel, "data-action": "next", onClick: (event) => {
44
+ const newPage = page + 1 <= sources.length ? page + 1 : sources.length;
45
+ onNextClick && onNextClick(event, newPage);
46
+ handleNewPage(event, newPage);
47
+ }, children: (0, jsx_runtime_1.jsx)(react_core_1.Icon, { isInline: true, iconSize: "lg", children: (0, jsx_runtime_1.jsx)("svg", { className: "pf-v6-svg", viewBox: "0 0 180 500", fill: "currentColor", "aria-hidden": "true", role: "img", width: "1em", height: "1em", children: (0, jsx_runtime_1.jsx)("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" }) }) }) })] }) }) }))))] })) }));
48
+ };
49
+ exports.default = SourcesCardBase;
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';