@patternfly/chatbot 6.3.0-prerelease.18 → 6.3.0-prerelease.19
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/FileDropZone/FileDropZone.d.ts +7 -0
- package/dist/cjs/FileDropZone/FileDropZone.js +2 -2
- package/dist/cjs/FileDropZone/FileDropZone.test.js +26 -0
- package/dist/cjs/MessageBar/AttachButton.d.ts +7 -0
- package/dist/cjs/MessageBar/AttachButton.js +3 -2
- package/dist/cjs/MessageBar/AttachButton.test.js +24 -0
- package/dist/cjs/MessageBar/MessageBar.d.ts +7 -0
- package/dist/cjs/MessageBar/MessageBar.js +2 -2
- package/dist/esm/FileDropZone/FileDropZone.d.ts +7 -0
- package/dist/esm/FileDropZone/FileDropZone.js +2 -2
- package/dist/esm/FileDropZone/FileDropZone.test.js +26 -0
- package/dist/esm/MessageBar/AttachButton.d.ts +7 -0
- package/dist/esm/MessageBar/AttachButton.js +3 -2
- package/dist/esm/MessageBar/AttachButton.test.js +24 -0
- package/dist/esm/MessageBar/MessageBar.d.ts +7 -0
- package/dist/esm/MessageBar/MessageBar.js +2 -2
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachment.tsx +10 -1
- package/src/FileDropZone/FileDropZone.test.tsx +29 -0
- package/src/FileDropZone/FileDropZone.tsx +9 -0
- package/src/MessageBar/AttachButton.test.tsx +45 -0
- package/src/MessageBar/AttachButton.tsx +10 -2
- package/src/MessageBar/MessageBar.tsx +10 -0
@@ -1,6 +1,7 @@
|
|
1
1
|
import { DropEvent } from '@patternfly/react-core';
|
2
2
|
import type { FunctionComponent } from 'react';
|
3
3
|
import { ChatbotDisplayMode } from '../Chatbot';
|
4
|
+
import { Accept } from 'react-dropzone/.';
|
4
5
|
export interface FileDropZoneProps {
|
5
6
|
/** Content displayed when the drop zone is not currently in use */
|
6
7
|
children?: React.ReactNode;
|
@@ -10,6 +11,12 @@ export interface FileDropZoneProps {
|
|
10
11
|
infoText?: string;
|
11
12
|
/** When files are dropped or uploaded this callback will be called with all accepted files */
|
12
13
|
onFileDrop: (event: DropEvent, data: File[]) => void;
|
14
|
+
/** Specifies the file types accepted by the attachment upload component.
|
15
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
16
|
+
* For example,
|
17
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
18
|
+
**/
|
19
|
+
allowedFileTypes?: Accept;
|
13
20
|
/** Display mode for the Chatbot parent; this influences the styles applied */
|
14
21
|
displayMode?: ChatbotDisplayMode;
|
15
22
|
}
|
@@ -17,9 +17,9 @@ const react_1 = require("react");
|
|
17
17
|
const Chatbot_1 = require("../Chatbot");
|
18
18
|
const react_icons_1 = require("@patternfly/react-icons");
|
19
19
|
const FileDropZone = (_a) => {
|
20
|
-
var { children, className, infoText = 'Maximum file size is 25 MB', onFileDrop, displayMode = Chatbot_1.ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "displayMode"]);
|
20
|
+
var { children, className, infoText = 'Maximum file size is 25 MB', onFileDrop, allowedFileTypes, displayMode = Chatbot_1.ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "allowedFileTypes", "displayMode"]);
|
21
21
|
const [showDropZone, setShowDropZone] = (0, react_1.useState)(false);
|
22
22
|
const renderDropZone = () => ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(react_core_1.MultipleFileUploadMain, { titleIcon: (0, jsx_runtime_1.jsx)(react_icons_1.UploadIcon, {}), titleText: "Drag and drop your file here", infoText: infoText, isUploadButtonHidden: true }) }));
|
23
|
-
return ((0, jsx_runtime_1.jsx)(react_core_1.MultipleFileUpload, { dropzoneProps: Object.assign({ onDrop: () => setShowDropZone(false) }, props), onDragEnter: () => setShowDropZone(true), onDragLeave: () => setShowDropZone(false), onFileDrop: onFileDrop, className: `pf-chatbot__dropzone pf-chatbot__dropzone--${displayMode} pf-chatbot__dropzone--${showDropZone ? 'visible' : 'invisible'} ${className ? className : ''}`, children: showDropZone ? renderDropZone() : children }));
|
23
|
+
return ((0, jsx_runtime_1.jsx)(react_core_1.MultipleFileUpload, { dropzoneProps: Object.assign({ accept: allowedFileTypes, onDrop: () => setShowDropZone(false) }, props), onDragEnter: () => setShowDropZone(true), onDragLeave: () => setShowDropZone(false), onFileDrop: onFileDrop, className: `pf-chatbot__dropzone pf-chatbot__dropzone--${displayMode} pf-chatbot__dropzone--${showDropZone ? 'visible' : 'invisible'} ${className ? className : ''}`, children: showDropZone ? renderDropZone() : children }));
|
24
24
|
};
|
25
25
|
exports.default = FileDropZone;
|
@@ -1,4 +1,13 @@
|
|
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
|
+
};
|
2
11
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
13
|
};
|
@@ -7,6 +16,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
16
|
const react_1 = require("@testing-library/react");
|
8
17
|
require("@testing-library/jest-dom");
|
9
18
|
const FileDropZone_1 = __importDefault(require("./FileDropZone"));
|
19
|
+
const user_event_1 = __importDefault(require("@testing-library/user-event"));
|
10
20
|
describe('FileDropZone', () => {
|
11
21
|
it('should render file drop zone', () => {
|
12
22
|
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn() }));
|
@@ -16,4 +26,20 @@ describe('FileDropZone', () => {
|
|
16
26
|
(0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn(), children: "Hi" }));
|
17
27
|
expect(react_1.screen.getByText('Hi')).toBeTruthy();
|
18
28
|
});
|
29
|
+
it('should call onFileDrop when file type is accepted', () => __awaiter(void 0, void 0, void 0, function* () {
|
30
|
+
const onFileDrop = jest.fn();
|
31
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { "data-testid": "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onFileDrop: onFileDrop }));
|
32
|
+
const file = new File(['Test'], 'example.text', { type: 'text/plain' });
|
33
|
+
const fileInput = container.querySelector('input[type="file"]');
|
34
|
+
yield user_event_1.default.upload(fileInput, file);
|
35
|
+
expect(onFileDrop).toHaveBeenCalled();
|
36
|
+
}));
|
37
|
+
it('should not call onFileDrop when file type is not accepted', () => __awaiter(void 0, void 0, void 0, function* () {
|
38
|
+
const onFileDrop = jest.fn();
|
39
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { "data-testid": "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onFileDrop: onFileDrop }));
|
40
|
+
const file = new File(['[]'], 'example.json', { type: 'application/json' });
|
41
|
+
const fileInput = container.querySelector('input[type="file"]');
|
42
|
+
yield user_event_1.default.upload(fileInput, file);
|
43
|
+
expect(onFileDrop).not.toHaveBeenCalled();
|
44
|
+
}));
|
19
45
|
});
|
@@ -1,9 +1,16 @@
|
|
1
1
|
import { ButtonProps, DropEvent, TooltipProps } from '@patternfly/react-core';
|
2
|
+
import { Accept } from 'react-dropzone';
|
2
3
|
export interface AttachButtonProps extends ButtonProps {
|
3
4
|
/** Callback for when button is clicked */
|
4
5
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
5
6
|
/** Callback function for AttachButton when an attachment is made */
|
6
7
|
onAttachAccepted?: (data: File[], event: DropEvent) => void;
|
8
|
+
/** Specifies the file types accepted by the attachment upload component.
|
9
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
10
|
+
* For example,
|
11
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
12
|
+
**/
|
13
|
+
allowedFileTypes?: Accept;
|
7
14
|
/** Class name for AttachButton */
|
8
15
|
className?: string;
|
9
16
|
/** Props to control if the AttachButton should be disabled */
|
@@ -19,10 +19,11 @@ const react_core_1 = require("@patternfly/react-core");
|
|
19
19
|
const react_dropzone_1 = require("react-dropzone");
|
20
20
|
const paperclip_icon_1 = require("@patternfly/react-icons/dist/esm/icons/paperclip-icon");
|
21
21
|
const AttachButtonBase = (_a) => {
|
22
|
-
var { onAttachAccepted, onClick, isDisabled, className, tooltipProps, innerRef, tooltipContent = 'Attach', inputTestId, isCompact } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact"]);
|
22
|
+
var { onAttachAccepted, onClick, isDisabled, className, tooltipProps, innerRef, tooltipContent = 'Attach', inputTestId, isCompact, allowedFileTypes } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact", "allowedFileTypes"]);
|
23
23
|
const { open, getInputProps } = (0, react_dropzone_1.useDropzone)({
|
24
24
|
multiple: true,
|
25
|
-
onDropAccepted: onAttachAccepted
|
25
|
+
onDropAccepted: onAttachAccepted,
|
26
|
+
accept: allowedFileTypes
|
26
27
|
});
|
27
28
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", Object.assign({ "data-testid": inputTestId }, getInputProps(), { hidden: true })), (0, jsx_runtime_1.jsx)(react_core_1.Tooltip, Object.assign({ id: "pf-chatbot__tooltip--attach", content: tooltipContent, position: "top", entryDelay: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.entryDelay) || 0, exitDelay: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.exitDelay) || 0, distance: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.distance) || 8, animationDuration: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.animationDuration) || 0,
|
28
29
|
// prevents VO announcements of both aria label and tooltip
|
@@ -67,4 +67,28 @@ describe('Attach button', () => {
|
|
67
67
|
(0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { isCompact: true, "data-testid": "button" }));
|
68
68
|
expect(react_1.screen.getByTestId('button')).toHaveClass('pf-m-compact');
|
69
69
|
});
|
70
|
+
it('should set correct accept attribute on file input', () => __awaiter(void 0, void 0, void 0, function* () {
|
71
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] } }));
|
72
|
+
yield user_event_1.default.click(react_1.screen.getByRole('button', { name: 'Attach' }));
|
73
|
+
const input = react_1.screen.getByTestId('input');
|
74
|
+
expect(input).toHaveAttribute('accept', 'text/plain,.txt');
|
75
|
+
}));
|
76
|
+
it('should call onAttachAccepted when file type is accepted', () => __awaiter(void 0, void 0, void 0, function* () {
|
77
|
+
const onAttachAccepted = jest.fn();
|
78
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onAttachAccepted: onAttachAccepted }));
|
79
|
+
const file = new File(['hello'], 'example.txt', { type: 'text/plain' });
|
80
|
+
const input = react_1.screen.getByTestId('input');
|
81
|
+
yield user_event_1.default.upload(input, file);
|
82
|
+
expect(onAttachAccepted).toHaveBeenCalled();
|
83
|
+
const [attachedFile] = onAttachAccepted.mock.calls[0][0];
|
84
|
+
expect(attachedFile).toEqual(file);
|
85
|
+
}));
|
86
|
+
it('should not call onAttachAccepted when file type is not accepted', () => __awaiter(void 0, void 0, void 0, function* () {
|
87
|
+
const onAttachAccepted = jest.fn();
|
88
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onAttachAccepted: onAttachAccepted }));
|
89
|
+
const file = new File(['[]'], 'example.json', { type: 'application/json' });
|
90
|
+
const input = react_1.screen.getByTestId('input');
|
91
|
+
yield user_event_1.default.upload(input, file);
|
92
|
+
expect(onAttachAccepted).not.toHaveBeenCalled();
|
93
|
+
}));
|
70
94
|
});
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import type { FunctionComponent } from 'react';
|
2
|
+
import { Accept } from 'react-dropzone/.';
|
2
3
|
import { ButtonProps, DropEvent, TextAreaProps, TooltipProps } from '@patternfly/react-core';
|
3
4
|
import { ChatbotDisplayMode } from '../Chatbot';
|
4
5
|
export interface MessageBarWithAttachMenuProps {
|
@@ -79,6 +80,12 @@ export interface MessageBarProps extends TextAreaProps {
|
|
79
80
|
/** Display mode of chatbot, if you want to message bar to resize when the display mode changes */
|
80
81
|
displayMode?: ChatbotDisplayMode;
|
81
82
|
isCompact?: boolean;
|
83
|
+
/** Specifies the file types accepted by the attachment upload component.
|
84
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
85
|
+
* For example,
|
86
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
87
|
+
**/
|
88
|
+
allowedFileTypes?: Accept;
|
82
89
|
}
|
83
90
|
export declare const MessageBar: FunctionComponent<MessageBarProps>;
|
84
91
|
export default MessageBar;
|
@@ -25,7 +25,7 @@ const AttachButton_1 = require("./AttachButton");
|
|
25
25
|
const AttachMenu_1 = __importDefault(require("../AttachMenu"));
|
26
26
|
const StopButton_1 = __importDefault(require("./StopButton"));
|
27
27
|
const MessageBar = (_a) => {
|
28
|
-
var { onSendMessage, className, alwayShowSendButton, placeholder = 'Send a message...', hasAttachButton = true, hasMicrophoneButton, listeningText = 'Listening', handleAttach, attachMenuProps, isSendButtonDisabled, handleStopButton, hasStopButton, buttonProps, onChange, displayMode, value, isCompact = false } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact"]);
|
28
|
+
var { onSendMessage, className, alwayShowSendButton, placeholder = 'Send a message...', hasAttachButton = true, hasMicrophoneButton, listeningText = 'Listening', handleAttach, attachMenuProps, isSendButtonDisabled, handleStopButton, hasStopButton, buttonProps, onChange, displayMode, value, isCompact = false, allowedFileTypes } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact", "allowedFileTypes"]);
|
29
29
|
// Text Input
|
30
30
|
// --------------------------------------------------------------------------
|
31
31
|
const [message, setMessage] = (0, react_1.useState)(value !== null && value !== void 0 ? value : '');
|
@@ -180,7 +180,7 @@ const MessageBar = (_a) => {
|
|
180
180
|
if (hasStopButton && handleStopButton) {
|
181
181
|
return ((0, jsx_runtime_1.jsx)(StopButton_1.default, Object.assign({ onClick: handleStopButton, tooltipContent: (_a = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.stop) === null || _a === void 0 ? void 0 : _a.tooltipContent, isCompact: isCompact, tooltipProps: (_b = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.stop) === null || _b === void 0 ? void 0 : _b.tooltipProps }, (_c = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.stop) === null || _c === void 0 ? void 0 : _c.props)));
|
182
182
|
}
|
183
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [attachMenuProps && ((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, Object.assign({ ref: attachButtonRef, onClick: handleAttachMenuToggle, isDisabled: isListeningMessage, tooltipContent: (_d = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _d === void 0 ? void 0 : _d.tooltipContent, isCompact: isCompact, tooltipProps: (_e = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _e === void 0 ? void 0 : _e.tooltipProps }, (_f = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _f === void 0 ? void 0 : _f.props))), !attachMenuProps && hasAttachButton && ((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, Object.assign({ onAttachAccepted: handleAttach, isDisabled: isListeningMessage, tooltipContent: (_g = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _g === void 0 ? void 0 : _g.tooltipContent, inputTestId: (_h = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _h === void 0 ? void 0 : _h.inputTestId, isCompact: isCompact, tooltipProps: (_j = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _j === void 0 ? void 0 : _j.tooltipProps }, (_k = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _k === void 0 ? void 0 : _k.props))), hasMicrophoneButton && ((0, jsx_runtime_1.jsx)(MicrophoneButton_1.default, Object.assign({ isListening: isListeningMessage, onIsListeningChange: setIsListeningMessage, onSpeechRecognition: handleSpeechRecognition, tooltipContent: (_l = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _l === void 0 ? void 0 : _l.tooltipContent, language: (_m = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _m === void 0 ? void 0 : _m.language, isCompact: isCompact, tooltipProps: (_o = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _o === void 0 ? void 0 : _o.tooltipProps }, (_p = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _p === void 0 ? void 0 : _p.props))), (alwayShowSendButton || message) && ((0, jsx_runtime_1.jsx)(SendButton_1.default, Object.assign({ value: message, onClick: () => handleSend(message), isDisabled: isSendButtonDisabled, tooltipContent: (_q = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _q === void 0 ? void 0 : _q.tooltipContent, isCompact: isCompact, tooltipProps: (_r = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _r === void 0 ? void 0 : _r.tooltipProps }, (_s = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _s === void 0 ? void 0 : _s.props)))] }));
|
183
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [attachMenuProps && ((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, Object.assign({ ref: attachButtonRef, onClick: handleAttachMenuToggle, isDisabled: isListeningMessage, tooltipContent: (_d = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _d === void 0 ? void 0 : _d.tooltipContent, isCompact: isCompact, tooltipProps: (_e = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _e === void 0 ? void 0 : _e.tooltipProps, allowedFileTypes: allowedFileTypes }, (_f = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _f === void 0 ? void 0 : _f.props))), !attachMenuProps && hasAttachButton && ((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, Object.assign({ onAttachAccepted: handleAttach, isDisabled: isListeningMessage, tooltipContent: (_g = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _g === void 0 ? void 0 : _g.tooltipContent, inputTestId: (_h = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _h === void 0 ? void 0 : _h.inputTestId, isCompact: isCompact, tooltipProps: (_j = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _j === void 0 ? void 0 : _j.tooltipProps, allowedFileTypes: allowedFileTypes }, (_k = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _k === void 0 ? void 0 : _k.props))), hasMicrophoneButton && ((0, jsx_runtime_1.jsx)(MicrophoneButton_1.default, Object.assign({ isListening: isListeningMessage, onIsListeningChange: setIsListeningMessage, onSpeechRecognition: handleSpeechRecognition, tooltipContent: (_l = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _l === void 0 ? void 0 : _l.tooltipContent, language: (_m = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _m === void 0 ? void 0 : _m.language, isCompact: isCompact, tooltipProps: (_o = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _o === void 0 ? void 0 : _o.tooltipProps }, (_p = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _p === void 0 ? void 0 : _p.props))), (alwayShowSendButton || message) && ((0, jsx_runtime_1.jsx)(SendButton_1.default, Object.assign({ value: message, onClick: () => handleSend(message), isDisabled: isSendButtonDisabled, tooltipContent: (_q = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _q === void 0 ? void 0 : _q.tooltipContent, isCompact: isCompact, tooltipProps: (_r = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _r === void 0 ? void 0 : _r.tooltipProps }, (_s = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _s === void 0 ? void 0 : _s.props)))] }));
|
184
184
|
};
|
185
185
|
const messageBarContents = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: `pf-chatbot__message-bar-input ${isCompact ? 'pf-m-compact' : ''}`, children: (0, jsx_runtime_1.jsx)(react_core_1.TextArea, Object.assign({ className: "pf-chatbot__message-textarea", value: message, onChange: handleChange, "aria-label": isListeningMessage ? listeningText : placeholder, placeholder: isListeningMessage ? listeningText : placeholder, ref: textareaRef, onKeyDown: handleKeyDown }, props)) }), (0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__message-bar-actions", children: renderButtons() })] }));
|
186
186
|
if (attachMenuProps) {
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { DropEvent } from '@patternfly/react-core';
|
2
2
|
import type { FunctionComponent } from 'react';
|
3
3
|
import { ChatbotDisplayMode } from '../Chatbot';
|
4
|
+
import { Accept } from 'react-dropzone/.';
|
4
5
|
export interface FileDropZoneProps {
|
5
6
|
/** Content displayed when the drop zone is not currently in use */
|
6
7
|
children?: React.ReactNode;
|
@@ -10,6 +11,12 @@ export interface FileDropZoneProps {
|
|
10
11
|
infoText?: string;
|
11
12
|
/** When files are dropped or uploaded this callback will be called with all accepted files */
|
12
13
|
onFileDrop: (event: DropEvent, data: File[]) => void;
|
14
|
+
/** Specifies the file types accepted by the attachment upload component.
|
15
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
16
|
+
* For example,
|
17
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
18
|
+
**/
|
19
|
+
allowedFileTypes?: Accept;
|
13
20
|
/** Display mode for the Chatbot parent; this influences the styles applied */
|
14
21
|
displayMode?: ChatbotDisplayMode;
|
15
22
|
}
|
@@ -15,9 +15,9 @@ import { useState } from 'react';
|
|
15
15
|
import { ChatbotDisplayMode } from '../Chatbot';
|
16
16
|
import { UploadIcon } from '@patternfly/react-icons';
|
17
17
|
const FileDropZone = (_a) => {
|
18
|
-
var { children, className, infoText = 'Maximum file size is 25 MB', onFileDrop, displayMode = ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "displayMode"]);
|
18
|
+
var { children, className, infoText = 'Maximum file size is 25 MB', onFileDrop, allowedFileTypes, displayMode = ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "allowedFileTypes", "displayMode"]);
|
19
19
|
const [showDropZone, setShowDropZone] = useState(false);
|
20
20
|
const renderDropZone = () => (_jsx(_Fragment, { children: _jsx(MultipleFileUploadMain, { titleIcon: _jsx(UploadIcon, {}), titleText: "Drag and drop your file here", infoText: infoText, isUploadButtonHidden: true }) }));
|
21
|
-
return (_jsx(MultipleFileUpload, { dropzoneProps: Object.assign({ onDrop: () => setShowDropZone(false) }, props), onDragEnter: () => setShowDropZone(true), onDragLeave: () => setShowDropZone(false), onFileDrop: onFileDrop, className: `pf-chatbot__dropzone pf-chatbot__dropzone--${displayMode} pf-chatbot__dropzone--${showDropZone ? 'visible' : 'invisible'} ${className ? className : ''}`, children: showDropZone ? renderDropZone() : children }));
|
21
|
+
return (_jsx(MultipleFileUpload, { dropzoneProps: Object.assign({ accept: allowedFileTypes, onDrop: () => setShowDropZone(false) }, props), onDragEnter: () => setShowDropZone(true), onDragLeave: () => setShowDropZone(false), onFileDrop: onFileDrop, className: `pf-chatbot__dropzone pf-chatbot__dropzone--${displayMode} pf-chatbot__dropzone--${showDropZone ? 'visible' : 'invisible'} ${className ? className : ''}`, children: showDropZone ? renderDropZone() : children }));
|
22
22
|
};
|
23
23
|
export default FileDropZone;
|
@@ -1,7 +1,17 @@
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
8
|
+
});
|
9
|
+
};
|
1
10
|
import { jsx as _jsx } from "react/jsx-runtime";
|
2
11
|
import { render, screen } from '@testing-library/react';
|
3
12
|
import '@testing-library/jest-dom';
|
4
13
|
import FileDropZone from './FileDropZone';
|
14
|
+
import userEvent from '@testing-library/user-event';
|
5
15
|
describe('FileDropZone', () => {
|
6
16
|
it('should render file drop zone', () => {
|
7
17
|
const { container } = render(_jsx(FileDropZone, { onFileDrop: jest.fn() }));
|
@@ -11,4 +21,20 @@ describe('FileDropZone', () => {
|
|
11
21
|
render(_jsx(FileDropZone, { onFileDrop: jest.fn(), children: "Hi" }));
|
12
22
|
expect(screen.getByText('Hi')).toBeTruthy();
|
13
23
|
});
|
24
|
+
it('should call onFileDrop when file type is accepted', () => __awaiter(void 0, void 0, void 0, function* () {
|
25
|
+
const onFileDrop = jest.fn();
|
26
|
+
const { container } = render(_jsx(FileDropZone, { "data-testid": "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onFileDrop: onFileDrop }));
|
27
|
+
const file = new File(['Test'], 'example.text', { type: 'text/plain' });
|
28
|
+
const fileInput = container.querySelector('input[type="file"]');
|
29
|
+
yield userEvent.upload(fileInput, file);
|
30
|
+
expect(onFileDrop).toHaveBeenCalled();
|
31
|
+
}));
|
32
|
+
it('should not call onFileDrop when file type is not accepted', () => __awaiter(void 0, void 0, void 0, function* () {
|
33
|
+
const onFileDrop = jest.fn();
|
34
|
+
const { container } = render(_jsx(FileDropZone, { "data-testid": "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onFileDrop: onFileDrop }));
|
35
|
+
const file = new File(['[]'], 'example.json', { type: 'application/json' });
|
36
|
+
const fileInput = container.querySelector('input[type="file"]');
|
37
|
+
yield userEvent.upload(fileInput, file);
|
38
|
+
expect(onFileDrop).not.toHaveBeenCalled();
|
39
|
+
}));
|
14
40
|
});
|
@@ -1,9 +1,16 @@
|
|
1
1
|
import { ButtonProps, DropEvent, TooltipProps } from '@patternfly/react-core';
|
2
|
+
import { Accept } from 'react-dropzone';
|
2
3
|
export interface AttachButtonProps extends ButtonProps {
|
3
4
|
/** Callback for when button is clicked */
|
4
5
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
5
6
|
/** Callback function for AttachButton when an attachment is made */
|
6
7
|
onAttachAccepted?: (data: File[], event: DropEvent) => void;
|
8
|
+
/** Specifies the file types accepted by the attachment upload component.
|
9
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
10
|
+
* For example,
|
11
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
12
|
+
**/
|
13
|
+
allowedFileTypes?: Accept;
|
7
14
|
/** Class name for AttachButton */
|
8
15
|
className?: string;
|
9
16
|
/** Props to control if the AttachButton should be disabled */
|
@@ -16,10 +16,11 @@ import { Button, Icon, Tooltip } from '@patternfly/react-core';
|
|
16
16
|
import { useDropzone } from 'react-dropzone';
|
17
17
|
import { PaperclipIcon } from '@patternfly/react-icons/dist/esm/icons/paperclip-icon';
|
18
18
|
const AttachButtonBase = (_a) => {
|
19
|
-
var { onAttachAccepted, onClick, isDisabled, className, tooltipProps, innerRef, tooltipContent = 'Attach', inputTestId, isCompact } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact"]);
|
19
|
+
var { onAttachAccepted, onClick, isDisabled, className, tooltipProps, innerRef, tooltipContent = 'Attach', inputTestId, isCompact, allowedFileTypes } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact", "allowedFileTypes"]);
|
20
20
|
const { open, getInputProps } = useDropzone({
|
21
21
|
multiple: true,
|
22
|
-
onDropAccepted: onAttachAccepted
|
22
|
+
onDropAccepted: onAttachAccepted,
|
23
|
+
accept: allowedFileTypes
|
23
24
|
});
|
24
25
|
return (_jsxs(_Fragment, { children: [_jsx("input", Object.assign({ "data-testid": inputTestId }, getInputProps(), { hidden: true })), _jsx(Tooltip, Object.assign({ id: "pf-chatbot__tooltip--attach", content: tooltipContent, position: "top", entryDelay: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.entryDelay) || 0, exitDelay: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.exitDelay) || 0, distance: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.distance) || 8, animationDuration: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.animationDuration) || 0,
|
25
26
|
// prevents VO announcements of both aria label and tooltip
|
@@ -62,4 +62,28 @@ describe('Attach button', () => {
|
|
62
62
|
render(_jsx(AttachButton, { isCompact: true, "data-testid": "button" }));
|
63
63
|
expect(screen.getByTestId('button')).toHaveClass('pf-m-compact');
|
64
64
|
});
|
65
|
+
it('should set correct accept attribute on file input', () => __awaiter(void 0, void 0, void 0, function* () {
|
66
|
+
render(_jsx(AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] } }));
|
67
|
+
yield userEvent.click(screen.getByRole('button', { name: 'Attach' }));
|
68
|
+
const input = screen.getByTestId('input');
|
69
|
+
expect(input).toHaveAttribute('accept', 'text/plain,.txt');
|
70
|
+
}));
|
71
|
+
it('should call onAttachAccepted when file type is accepted', () => __awaiter(void 0, void 0, void 0, function* () {
|
72
|
+
const onAttachAccepted = jest.fn();
|
73
|
+
render(_jsx(AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onAttachAccepted: onAttachAccepted }));
|
74
|
+
const file = new File(['hello'], 'example.txt', { type: 'text/plain' });
|
75
|
+
const input = screen.getByTestId('input');
|
76
|
+
yield userEvent.upload(input, file);
|
77
|
+
expect(onAttachAccepted).toHaveBeenCalled();
|
78
|
+
const [attachedFile] = onAttachAccepted.mock.calls[0][0];
|
79
|
+
expect(attachedFile).toEqual(file);
|
80
|
+
}));
|
81
|
+
it('should not call onAttachAccepted when file type is not accepted', () => __awaiter(void 0, void 0, void 0, function* () {
|
82
|
+
const onAttachAccepted = jest.fn();
|
83
|
+
render(_jsx(AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onAttachAccepted: onAttachAccepted }));
|
84
|
+
const file = new File(['[]'], 'example.json', { type: 'application/json' });
|
85
|
+
const input = screen.getByTestId('input');
|
86
|
+
yield userEvent.upload(input, file);
|
87
|
+
expect(onAttachAccepted).not.toHaveBeenCalled();
|
88
|
+
}));
|
65
89
|
});
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import type { FunctionComponent } from 'react';
|
2
|
+
import { Accept } from 'react-dropzone/.';
|
2
3
|
import { ButtonProps, DropEvent, TextAreaProps, TooltipProps } from '@patternfly/react-core';
|
3
4
|
import { ChatbotDisplayMode } from '../Chatbot';
|
4
5
|
export interface MessageBarWithAttachMenuProps {
|
@@ -79,6 +80,12 @@ export interface MessageBarProps extends TextAreaProps {
|
|
79
80
|
/** Display mode of chatbot, if you want to message bar to resize when the display mode changes */
|
80
81
|
displayMode?: ChatbotDisplayMode;
|
81
82
|
isCompact?: boolean;
|
83
|
+
/** Specifies the file types accepted by the attachment upload component.
|
84
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
85
|
+
* For example,
|
86
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
87
|
+
**/
|
88
|
+
allowedFileTypes?: Accept;
|
82
89
|
}
|
83
90
|
export declare const MessageBar: FunctionComponent<MessageBarProps>;
|
84
91
|
export default MessageBar;
|
@@ -19,7 +19,7 @@ import { AttachButton } from './AttachButton';
|
|
19
19
|
import AttachMenu from '../AttachMenu';
|
20
20
|
import StopButton from './StopButton';
|
21
21
|
export const MessageBar = (_a) => {
|
22
|
-
var { onSendMessage, className, alwayShowSendButton, placeholder = 'Send a message...', hasAttachButton = true, hasMicrophoneButton, listeningText = 'Listening', handleAttach, attachMenuProps, isSendButtonDisabled, handleStopButton, hasStopButton, buttonProps, onChange, displayMode, value, isCompact = false } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact"]);
|
22
|
+
var { onSendMessage, className, alwayShowSendButton, placeholder = 'Send a message...', hasAttachButton = true, hasMicrophoneButton, listeningText = 'Listening', handleAttach, attachMenuProps, isSendButtonDisabled, handleStopButton, hasStopButton, buttonProps, onChange, displayMode, value, isCompact = false, allowedFileTypes } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact", "allowedFileTypes"]);
|
23
23
|
// Text Input
|
24
24
|
// --------------------------------------------------------------------------
|
25
25
|
const [message, setMessage] = useState(value !== null && value !== void 0 ? value : '');
|
@@ -174,7 +174,7 @@ export const MessageBar = (_a) => {
|
|
174
174
|
if (hasStopButton && handleStopButton) {
|
175
175
|
return (_jsx(StopButton, Object.assign({ onClick: handleStopButton, tooltipContent: (_a = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.stop) === null || _a === void 0 ? void 0 : _a.tooltipContent, isCompact: isCompact, tooltipProps: (_b = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.stop) === null || _b === void 0 ? void 0 : _b.tooltipProps }, (_c = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.stop) === null || _c === void 0 ? void 0 : _c.props)));
|
176
176
|
}
|
177
|
-
return (_jsxs(_Fragment, { children: [attachMenuProps && (_jsx(AttachButton, Object.assign({ ref: attachButtonRef, onClick: handleAttachMenuToggle, isDisabled: isListeningMessage, tooltipContent: (_d = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _d === void 0 ? void 0 : _d.tooltipContent, isCompact: isCompact, tooltipProps: (_e = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _e === void 0 ? void 0 : _e.tooltipProps }, (_f = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _f === void 0 ? void 0 : _f.props))), !attachMenuProps && hasAttachButton && (_jsx(AttachButton, Object.assign({ onAttachAccepted: handleAttach, isDisabled: isListeningMessage, tooltipContent: (_g = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _g === void 0 ? void 0 : _g.tooltipContent, inputTestId: (_h = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _h === void 0 ? void 0 : _h.inputTestId, isCompact: isCompact, tooltipProps: (_j = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _j === void 0 ? void 0 : _j.tooltipProps }, (_k = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _k === void 0 ? void 0 : _k.props))), hasMicrophoneButton && (_jsx(MicrophoneButton, Object.assign({ isListening: isListeningMessage, onIsListeningChange: setIsListeningMessage, onSpeechRecognition: handleSpeechRecognition, tooltipContent: (_l = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _l === void 0 ? void 0 : _l.tooltipContent, language: (_m = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _m === void 0 ? void 0 : _m.language, isCompact: isCompact, tooltipProps: (_o = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _o === void 0 ? void 0 : _o.tooltipProps }, (_p = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _p === void 0 ? void 0 : _p.props))), (alwayShowSendButton || message) && (_jsx(SendButton, Object.assign({ value: message, onClick: () => handleSend(message), isDisabled: isSendButtonDisabled, tooltipContent: (_q = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _q === void 0 ? void 0 : _q.tooltipContent, isCompact: isCompact, tooltipProps: (_r = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _r === void 0 ? void 0 : _r.tooltipProps }, (_s = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _s === void 0 ? void 0 : _s.props)))] }));
|
177
|
+
return (_jsxs(_Fragment, { children: [attachMenuProps && (_jsx(AttachButton, Object.assign({ ref: attachButtonRef, onClick: handleAttachMenuToggle, isDisabled: isListeningMessage, tooltipContent: (_d = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _d === void 0 ? void 0 : _d.tooltipContent, isCompact: isCompact, tooltipProps: (_e = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _e === void 0 ? void 0 : _e.tooltipProps, allowedFileTypes: allowedFileTypes }, (_f = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _f === void 0 ? void 0 : _f.props))), !attachMenuProps && hasAttachButton && (_jsx(AttachButton, Object.assign({ onAttachAccepted: handleAttach, isDisabled: isListeningMessage, tooltipContent: (_g = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _g === void 0 ? void 0 : _g.tooltipContent, inputTestId: (_h = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _h === void 0 ? void 0 : _h.inputTestId, isCompact: isCompact, tooltipProps: (_j = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _j === void 0 ? void 0 : _j.tooltipProps, allowedFileTypes: allowedFileTypes }, (_k = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _k === void 0 ? void 0 : _k.props))), hasMicrophoneButton && (_jsx(MicrophoneButton, Object.assign({ isListening: isListeningMessage, onIsListeningChange: setIsListeningMessage, onSpeechRecognition: handleSpeechRecognition, tooltipContent: (_l = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _l === void 0 ? void 0 : _l.tooltipContent, language: (_m = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _m === void 0 ? void 0 : _m.language, isCompact: isCompact, tooltipProps: (_o = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _o === void 0 ? void 0 : _o.tooltipProps }, (_p = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _p === void 0 ? void 0 : _p.props))), (alwayShowSendButton || message) && (_jsx(SendButton, Object.assign({ value: message, onClick: () => handleSend(message), isDisabled: isSendButtonDisabled, tooltipContent: (_q = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _q === void 0 ? void 0 : _q.tooltipContent, isCompact: isCompact, tooltipProps: (_r = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _r === void 0 ? void 0 : _r.tooltipProps }, (_s = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _s === void 0 ? void 0 : _s.props)))] }));
|
178
178
|
};
|
179
179
|
const messageBarContents = (_jsxs(_Fragment, { children: [_jsx("div", { className: `pf-chatbot__message-bar-input ${isCompact ? 'pf-m-compact' : ''}`, children: _jsx(TextArea, Object.assign({ className: "pf-chatbot__message-textarea", value: message, onChange: handleChange, "aria-label": isListeningMessage ? listeningText : placeholder, placeholder: isListeningMessage ? listeningText : placeholder, ref: textareaRef, onKeyDown: handleKeyDown }, props)) }), _jsx("div", { className: "pf-chatbot__message-bar-actions", children: renderButtons() })] }));
|
180
180
|
if (attachMenuProps) {
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@patternfly/chatbot",
|
3
|
-
"version": "6.3.0-prerelease.
|
3
|
+
"version": "6.3.0-prerelease.19",
|
4
4
|
"description": "This library provides React components based on PatternFly 6 that can be used to build chatbots.",
|
5
5
|
"main": "dist/cjs/index.js",
|
6
6
|
"module": "dist/esm/index.js",
|
@@ -218,7 +218,16 @@ export const BasicDemo: FunctionComponent = () => {
|
|
218
218
|
</ChatbotHeaderOptionsDropdown>
|
219
219
|
</ChatbotHeaderActions>
|
220
220
|
</ChatbotHeader>
|
221
|
-
<FileDropZone
|
221
|
+
<FileDropZone
|
222
|
+
onFileDrop={handleFileDrop}
|
223
|
+
displayMode={displayMode}
|
224
|
+
infoText="Allowed file types are .json, .txt and .yaml and maximum file size is 25 MB."
|
225
|
+
allowedFileTypes={{
|
226
|
+
'text/plain': ['.txt'],
|
227
|
+
'application/json': ['.json'],
|
228
|
+
'application/yaml': ['.yaml', '.yml']
|
229
|
+
}}
|
230
|
+
>
|
222
231
|
<ChatbotContent>
|
223
232
|
<MessageBox>
|
224
233
|
{showAlert && (
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
2
2
|
import '@testing-library/jest-dom';
|
3
3
|
import FileDropZone from './FileDropZone';
|
4
|
+
import userEvent from '@testing-library/user-event';
|
4
5
|
|
5
6
|
describe('FileDropZone', () => {
|
6
7
|
it('should render file drop zone', () => {
|
@@ -11,4 +12,32 @@ describe('FileDropZone', () => {
|
|
11
12
|
render(<FileDropZone onFileDrop={jest.fn()}>Hi</FileDropZone>);
|
12
13
|
expect(screen.getByText('Hi')).toBeTruthy();
|
13
14
|
});
|
15
|
+
|
16
|
+
it('should call onFileDrop when file type is accepted', async () => {
|
17
|
+
const onFileDrop = jest.fn();
|
18
|
+
const { container } = render(
|
19
|
+
<FileDropZone data-testid="input" allowedFileTypes={{ 'text/plain': ['.txt'] }} onFileDrop={onFileDrop} />
|
20
|
+
);
|
21
|
+
|
22
|
+
const file = new File(['Test'], 'example.text', { type: 'text/plain' });
|
23
|
+
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
24
|
+
|
25
|
+
await userEvent.upload(fileInput, file);
|
26
|
+
|
27
|
+
expect(onFileDrop).toHaveBeenCalled();
|
28
|
+
});
|
29
|
+
|
30
|
+
it('should not call onFileDrop when file type is not accepted', async () => {
|
31
|
+
const onFileDrop = jest.fn();
|
32
|
+
const { container } = render(
|
33
|
+
<FileDropZone data-testid="input" allowedFileTypes={{ 'text/plain': ['.txt'] }} onFileDrop={onFileDrop} />
|
34
|
+
);
|
35
|
+
|
36
|
+
const file = new File(['[]'], 'example.json', { type: 'application/json' });
|
37
|
+
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
38
|
+
|
39
|
+
await userEvent.upload(fileInput, file);
|
40
|
+
|
41
|
+
expect(onFileDrop).not.toHaveBeenCalled();
|
42
|
+
});
|
14
43
|
});
|
@@ -3,6 +3,7 @@ import type { FunctionComponent } from 'react';
|
|
3
3
|
import { useState } from 'react';
|
4
4
|
import { ChatbotDisplayMode } from '../Chatbot';
|
5
5
|
import { UploadIcon } from '@patternfly/react-icons';
|
6
|
+
import { Accept } from 'react-dropzone/.';
|
6
7
|
|
7
8
|
export interface FileDropZoneProps {
|
8
9
|
/** Content displayed when the drop zone is not currently in use */
|
@@ -13,6 +14,12 @@ export interface FileDropZoneProps {
|
|
13
14
|
infoText?: string;
|
14
15
|
/** When files are dropped or uploaded this callback will be called with all accepted files */
|
15
16
|
onFileDrop: (event: DropEvent, data: File[]) => void;
|
17
|
+
/** Specifies the file types accepted by the attachment upload component.
|
18
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
19
|
+
* For example,
|
20
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
21
|
+
**/
|
22
|
+
allowedFileTypes?: Accept;
|
16
23
|
/** Display mode for the Chatbot parent; this influences the styles applied */
|
17
24
|
displayMode?: ChatbotDisplayMode;
|
18
25
|
}
|
@@ -22,6 +29,7 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
|
|
22
29
|
className,
|
23
30
|
infoText = 'Maximum file size is 25 MB',
|
24
31
|
onFileDrop,
|
32
|
+
allowedFileTypes,
|
25
33
|
displayMode = ChatbotDisplayMode.default,
|
26
34
|
...props
|
27
35
|
}: FileDropZoneProps) => {
|
@@ -41,6 +49,7 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
|
|
41
49
|
return (
|
42
50
|
<MultipleFileUpload
|
43
51
|
dropzoneProps={{
|
52
|
+
accept: allowedFileTypes,
|
44
53
|
onDrop: () => setShowDropZone(false),
|
45
54
|
...props
|
46
55
|
}}
|
@@ -53,4 +53,49 @@ describe('Attach button', () => {
|
|
53
53
|
render(<AttachButton isCompact data-testid="button" />);
|
54
54
|
expect(screen.getByTestId('button')).toHaveClass('pf-m-compact');
|
55
55
|
});
|
56
|
+
|
57
|
+
it('should set correct accept attribute on file input', async () => {
|
58
|
+
render(<AttachButton inputTestId="input" allowedFileTypes={{ 'text/plain': ['.txt'] }} />);
|
59
|
+
await userEvent.click(screen.getByRole('button', { name: 'Attach' }));
|
60
|
+
const input = screen.getByTestId('input') as HTMLInputElement;
|
61
|
+
expect(input).toHaveAttribute('accept', 'text/plain,.txt');
|
62
|
+
});
|
63
|
+
|
64
|
+
it('should call onAttachAccepted when file type is accepted', async () => {
|
65
|
+
const onAttachAccepted = jest.fn();
|
66
|
+
render(
|
67
|
+
<AttachButton
|
68
|
+
inputTestId="input"
|
69
|
+
allowedFileTypes={{ 'text/plain': ['.txt'] }}
|
70
|
+
onAttachAccepted={onAttachAccepted}
|
71
|
+
/>
|
72
|
+
);
|
73
|
+
|
74
|
+
const file = new File(['hello'], 'example.txt', { type: 'text/plain' });
|
75
|
+
const input = screen.getByTestId('input');
|
76
|
+
|
77
|
+
await userEvent.upload(input, file);
|
78
|
+
|
79
|
+
expect(onAttachAccepted).toHaveBeenCalled();
|
80
|
+
const [attachedFile] = onAttachAccepted.mock.calls[0][0];
|
81
|
+
expect(attachedFile).toEqual(file);
|
82
|
+
});
|
83
|
+
|
84
|
+
it('should not call onAttachAccepted when file type is not accepted', async () => {
|
85
|
+
const onAttachAccepted = jest.fn();
|
86
|
+
render(
|
87
|
+
<AttachButton
|
88
|
+
inputTestId="input"
|
89
|
+
allowedFileTypes={{ 'text/plain': ['.txt'] }}
|
90
|
+
onAttachAccepted={onAttachAccepted}
|
91
|
+
/>
|
92
|
+
);
|
93
|
+
|
94
|
+
const file = new File(['[]'], 'example.json', { type: 'application/json' });
|
95
|
+
const input = screen.getByTestId('input');
|
96
|
+
|
97
|
+
await userEvent.upload(input, file);
|
98
|
+
|
99
|
+
expect(onAttachAccepted).not.toHaveBeenCalled();
|
100
|
+
});
|
56
101
|
});
|
@@ -7,7 +7,7 @@ import { forwardRef } from 'react';
|
|
7
7
|
|
8
8
|
// Import PatternFly components
|
9
9
|
import { Button, ButtonProps, DropEvent, Icon, Tooltip, TooltipProps } from '@patternfly/react-core';
|
10
|
-
import { useDropzone } from 'react-dropzone';
|
10
|
+
import { Accept, useDropzone } from 'react-dropzone';
|
11
11
|
import { PaperclipIcon } from '@patternfly/react-icons/dist/esm/icons/paperclip-icon';
|
12
12
|
|
13
13
|
export interface AttachButtonProps extends ButtonProps {
|
@@ -15,6 +15,12 @@ export interface AttachButtonProps extends ButtonProps {
|
|
15
15
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
16
16
|
/** Callback function for AttachButton when an attachment is made */
|
17
17
|
onAttachAccepted?: (data: File[], event: DropEvent) => void;
|
18
|
+
/** Specifies the file types accepted by the attachment upload component.
|
19
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
20
|
+
* For example,
|
21
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
22
|
+
**/
|
23
|
+
allowedFileTypes?: Accept;
|
18
24
|
/** Class name for AttachButton */
|
19
25
|
className?: string;
|
20
26
|
/** Props to control if the AttachButton should be disabled */
|
@@ -40,11 +46,13 @@ const AttachButtonBase: FunctionComponent<AttachButtonProps> = ({
|
|
40
46
|
tooltipContent = 'Attach',
|
41
47
|
inputTestId,
|
42
48
|
isCompact,
|
49
|
+
allowedFileTypes,
|
43
50
|
...props
|
44
51
|
}: AttachButtonProps) => {
|
45
52
|
const { open, getInputProps } = useDropzone({
|
46
53
|
multiple: true,
|
47
|
-
onDropAccepted: onAttachAccepted
|
54
|
+
onDropAccepted: onAttachAccepted,
|
55
|
+
accept: allowedFileTypes
|
48
56
|
});
|
49
57
|
|
50
58
|
return (
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import type { ChangeEvent, FunctionComponent, KeyboardEvent as ReactKeyboardEvent } from 'react';
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
3
|
+
import { Accept } from 'react-dropzone/.';
|
3
4
|
import { ButtonProps, DropEvent, TextArea, TextAreaProps, TooltipProps } from '@patternfly/react-core';
|
4
5
|
|
5
6
|
// Import Chatbot components
|
@@ -78,6 +79,12 @@ export interface MessageBarProps extends TextAreaProps {
|
|
78
79
|
/** Display mode of chatbot, if you want to message bar to resize when the display mode changes */
|
79
80
|
displayMode?: ChatbotDisplayMode;
|
80
81
|
isCompact?: boolean;
|
82
|
+
/** Specifies the file types accepted by the attachment upload component.
|
83
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
84
|
+
* For example,
|
85
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
86
|
+
**/
|
87
|
+
allowedFileTypes?: Accept;
|
81
88
|
}
|
82
89
|
|
83
90
|
export const MessageBar: FunctionComponent<MessageBarProps> = ({
|
@@ -98,6 +105,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
|
|
98
105
|
displayMode,
|
99
106
|
value,
|
100
107
|
isCompact = false,
|
108
|
+
allowedFileTypes,
|
101
109
|
...props
|
102
110
|
}: MessageBarProps) => {
|
103
111
|
// Text Input
|
@@ -295,6 +303,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
|
|
295
303
|
tooltipContent={buttonProps?.attach?.tooltipContent}
|
296
304
|
isCompact={isCompact}
|
297
305
|
tooltipProps={buttonProps?.attach?.tooltipProps}
|
306
|
+
allowedFileTypes={allowedFileTypes}
|
298
307
|
{...buttonProps?.attach?.props}
|
299
308
|
/>
|
300
309
|
)}
|
@@ -306,6 +315,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
|
|
306
315
|
inputTestId={buttonProps?.attach?.inputTestId}
|
307
316
|
isCompact={isCompact}
|
308
317
|
tooltipProps={buttonProps?.attach?.tooltipProps}
|
318
|
+
allowedFileTypes={allowedFileTypes}
|
309
319
|
{...buttonProps?.attach?.props}
|
310
320
|
/>
|
311
321
|
)}
|