@patternfly/chatbot 6.3.0-prerelease.22 → 6.3.0-prerelease.23
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 +15 -1
- package/dist/cjs/FileDropZone/FileDropZone.js +7 -2
- package/dist/cjs/FileDropZone/FileDropZone.test.js +55 -0
- package/dist/cjs/MessageBar/AttachButton.d.ts +18 -1
- package/dist/cjs/MessageBar/AttachButton.js +4 -6
- package/dist/cjs/MessageBar/AttachButton.test.js +54 -0
- package/dist/cjs/MessageBar/MessageBar.d.ts +23 -7
- package/dist/cjs/MessageBar/MessageBar.js +2 -2
- package/dist/esm/FileDropZone/FileDropZone.d.ts +15 -1
- package/dist/esm/FileDropZone/FileDropZone.js +7 -2
- package/dist/esm/FileDropZone/FileDropZone.test.js +55 -0
- package/dist/esm/MessageBar/AttachButton.d.ts +18 -1
- package/dist/esm/MessageBar/AttachButton.js +4 -6
- package/dist/esm/MessageBar/AttachButton.test.js +54 -0
- package/dist/esm/MessageBar/MessageBar.d.ts +23 -7
- package/dist/esm/MessageBar/MessageBar.js +2 -2
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachment.tsx +19 -1
- package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachmentMenu.tsx +1 -0
- package/src/FileDropZone/FileDropZone.test.tsx +83 -0
- package/src/FileDropZone/FileDropZone.tsx +32 -2
- package/src/MessageBar/AttachButton.test.tsx +75 -0
- package/src/MessageBar/AttachButton.tsx +35 -2
- package/src/MessageBar/MessageBar.tsx +47 -7
@@ -1,7 +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
|
+
import { Accept, FileError, FileRejection } from 'react-dropzone/.';
|
5
5
|
export interface FileDropZoneProps {
|
6
6
|
/** Content displayed when the drop zone is not currently in use */
|
7
7
|
children?: React.ReactNode;
|
@@ -19,6 +19,20 @@ export interface FileDropZoneProps {
|
|
19
19
|
allowedFileTypes?: Accept;
|
20
20
|
/** Display mode for the Chatbot parent; this influences the styles applied */
|
21
21
|
displayMode?: ChatbotDisplayMode;
|
22
|
+
/** Minimum file size allowed */
|
23
|
+
minSize?: number;
|
24
|
+
/** Max file size allowed */
|
25
|
+
maxSize?: number;
|
26
|
+
/** Max number of files allowed */
|
27
|
+
maxFiles?: number;
|
28
|
+
/** Whether attachments are disabled */
|
29
|
+
isAttachmentDisabled?: boolean;
|
30
|
+
/** Callback when file(s) are attached */
|
31
|
+
onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
|
32
|
+
/** Callback function for AttachButton when an attachment fails */
|
33
|
+
onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
|
34
|
+
/** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
|
35
|
+
validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
|
22
36
|
}
|
23
37
|
declare const FileDropZone: FunctionComponent<FileDropZoneProps>;
|
24
38
|
export default FileDropZone;
|
@@ -17,9 +17,14 @@ 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, allowedFileTypes, displayMode = Chatbot_1.ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "allowedFileTypes", "displayMode"]);
|
20
|
+
var { children, className, infoText = 'Maximum file size is 25 MB', onFileDrop, allowedFileTypes, minSize, maxSize, maxFiles, isAttachmentDisabled, onAttach, onAttachRejected, validator, displayMode = Chatbot_1.ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "allowedFileTypes", "minSize", "maxSize", "maxFiles", "isAttachmentDisabled", "onAttach", "onAttachRejected", "validator", "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({ accept: allowedFileTypes, onDrop: (
|
23
|
+
return ((0, jsx_runtime_1.jsx)(react_core_1.MultipleFileUpload, { dropzoneProps: Object.assign({ accept: allowedFileTypes, onDrop: (acceptedFiles, fileRejections, event) => {
|
24
|
+
setShowDropZone(false);
|
25
|
+
onAttach && onAttach(acceptedFiles, fileRejections, event);
|
26
|
+
}, minSize,
|
27
|
+
maxSize,
|
28
|
+
maxFiles, disabled: isAttachmentDisabled, onDropRejected: onAttachRejected, validator }, 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
29
|
};
|
25
30
|
exports.default = FileDropZone;
|
@@ -42,4 +42,59 @@ describe('FileDropZone', () => {
|
|
42
42
|
yield user_event_1.default.upload(fileInput, file);
|
43
43
|
expect(onFileDrop).not.toHaveBeenCalled();
|
44
44
|
}));
|
45
|
+
it('should respect minSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
46
|
+
const onAttachRejected = jest.fn();
|
47
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn(), minSize: 1000, onAttachRejected: onAttachRejected }));
|
48
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
49
|
+
const fileInput = container.querySelector('input[type="file"]');
|
50
|
+
yield user_event_1.default.upload(fileInput, file);
|
51
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
52
|
+
}));
|
53
|
+
it('should respect maxSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
54
|
+
const onAttachRejected = jest.fn();
|
55
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn(), maxSize: 100, onAttachRejected: onAttachRejected }));
|
56
|
+
const largeContent = 'x'.repeat(200);
|
57
|
+
const file = new File([largeContent], 'example.txt', { type: 'text/plain' });
|
58
|
+
const fileInput = container.querySelector('input[type="file"]');
|
59
|
+
yield user_event_1.default.upload(fileInput, file);
|
60
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
61
|
+
}));
|
62
|
+
it('should respect maxFiles restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
63
|
+
const onAttachRejected = jest.fn();
|
64
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn(), maxFiles: 1, onAttachRejected: onAttachRejected }));
|
65
|
+
const files = [
|
66
|
+
new File(['Test1'], 'example1.txt', { type: 'text/plain' }),
|
67
|
+
new File(['Test2'], 'example2.txt', { type: 'text/plain' })
|
68
|
+
];
|
69
|
+
const fileInput = container.querySelector('input[type="file"]');
|
70
|
+
yield user_event_1.default.upload(fileInput, files);
|
71
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
72
|
+
}));
|
73
|
+
it('should be disabled when isAttachmentDisabled is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
74
|
+
const onFileDrop = jest.fn();
|
75
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: onFileDrop, isAttachmentDisabled: true }));
|
76
|
+
const file = new File(['Test'], 'example.text', { type: 'text/plain' });
|
77
|
+
const fileInput = container.querySelector('input[type="file"]');
|
78
|
+
yield user_event_1.default.upload(fileInput, file);
|
79
|
+
expect(onFileDrop).not.toHaveBeenCalled();
|
80
|
+
}));
|
81
|
+
it('should call onAttach when files are attached', () => __awaiter(void 0, void 0, void 0, function* () {
|
82
|
+
const onAttach = jest.fn();
|
83
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn(), onAttach: onAttach }));
|
84
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
85
|
+
const fileInput = container.querySelector('input[type="file"]');
|
86
|
+
yield user_event_1.default.upload(fileInput, file);
|
87
|
+
expect(onAttach).toHaveBeenCalled();
|
88
|
+
}));
|
89
|
+
it('should use custom validator when provided', () => __awaiter(void 0, void 0, void 0, function* () {
|
90
|
+
const validator = jest.fn().mockReturnValue({ message: 'Custom error' });
|
91
|
+
const onAttachRejected = jest.fn();
|
92
|
+
const onFileDrop = jest.fn();
|
93
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: onFileDrop, validator: validator, onAttachRejected: onAttachRejected }));
|
94
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
95
|
+
const fileInput = container.querySelector('input[type="file"]');
|
96
|
+
yield user_event_1.default.upload(fileInput, file);
|
97
|
+
expect(validator).toHaveBeenCalledWith(file);
|
98
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
99
|
+
}));
|
45
100
|
});
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { ButtonProps, DropEvent, TooltipProps } from '@patternfly/react-core';
|
2
|
-
import { Accept } from 'react-dropzone';
|
2
|
+
import { Accept, DropzoneOptions, FileError, FileRejection } from 'react-dropzone';
|
3
3
|
export interface AttachButtonProps extends ButtonProps {
|
4
4
|
/** Callback for when button is clicked */
|
5
5
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
@@ -23,6 +23,23 @@ export interface AttachButtonProps extends ButtonProps {
|
|
23
23
|
tooltipContent?: string;
|
24
24
|
/** Test id applied to input */
|
25
25
|
inputTestId?: string;
|
26
|
+
/** Whether button is compact */
|
26
27
|
isCompact?: boolean;
|
28
|
+
/** Minimum file size allowed */
|
29
|
+
minSize?: number;
|
30
|
+
/** Max file size allowed */
|
31
|
+
maxSize?: number;
|
32
|
+
/** Max number of files allowed */
|
33
|
+
maxFiles?: number;
|
34
|
+
/** Whether attachments are disabled */
|
35
|
+
isAttachmentDisabled?: boolean;
|
36
|
+
/** Callback when file(s) are attached */
|
37
|
+
onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
|
38
|
+
/** Callback function for AttachButton when an attachment fails */
|
39
|
+
onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
|
40
|
+
/** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
|
41
|
+
validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
|
42
|
+
/** Additional props passed to react-dropzone */
|
43
|
+
dropzoneProps?: DropzoneOptions;
|
27
44
|
}
|
28
45
|
export declare const AttachButton: import("react").ForwardRefExoticComponent<AttachButtonProps & import("react").RefAttributes<any>>;
|
@@ -19,12 +19,10 @@ 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, allowedFileTypes } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact", "allowedFileTypes"]);
|
23
|
-
const { open, getInputProps } = (0, react_dropzone_1.useDropzone)({
|
24
|
-
|
25
|
-
|
26
|
-
accept: allowedFileTypes
|
27
|
-
});
|
22
|
+
var { onAttachAccepted, onClick, isDisabled, className, tooltipProps, innerRef, tooltipContent = 'Attach', inputTestId, isCompact, allowedFileTypes, minSize, maxSize, maxFiles, isAttachmentDisabled, onAttach, onAttachRejected, validator, dropzoneProps } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact", "allowedFileTypes", "minSize", "maxSize", "maxFiles", "isAttachmentDisabled", "onAttach", "onAttachRejected", "validator", "dropzoneProps"]);
|
23
|
+
const { open, getInputProps } = (0, react_dropzone_1.useDropzone)(Object.assign({ multiple: true, onDropAccepted: onAttachAccepted, accept: allowedFileTypes, minSize,
|
24
|
+
maxSize,
|
25
|
+
maxFiles, disabled: isAttachmentDisabled, onDrop: onAttach, onDropRejected: onAttachRejected, validator }, dropzoneProps));
|
28
26
|
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,
|
29
27
|
// prevents VO announcements of both aria label and tooltip
|
30
28
|
aria: "none" }, tooltipProps, { children: (0, jsx_runtime_1.jsx)(react_core_1.Button, Object.assign({ variant: "plain", ref: innerRef, className: `pf-chatbot__button--attach ${isCompact ? 'pf-m-compact' : ''} ${className !== null && className !== void 0 ? className : ''}`, "aria-label": props['aria-label'] || 'Attach', isDisabled: isDisabled, onClick: onClick !== null && onClick !== void 0 ? onClick : open, icon: (0, jsx_runtime_1.jsx)(react_core_1.Icon, { iconSize: isCompact ? 'lg' : 'xl', isInline: true, children: (0, jsx_runtime_1.jsx)(paperclip_icon_1.PaperclipIcon, {}) }), size: isCompact ? 'sm' : undefined }, props)) }))] }));
|
@@ -91,4 +91,58 @@ describe('Attach button', () => {
|
|
91
91
|
yield user_event_1.default.upload(input, file);
|
92
92
|
expect(onAttachAccepted).not.toHaveBeenCalled();
|
93
93
|
}));
|
94
|
+
it('should respect minSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
95
|
+
const onAttachRejected = jest.fn();
|
96
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", minSize: 1000, onAttachRejected: onAttachRejected }));
|
97
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
98
|
+
const input = react_1.screen.getByTestId('input');
|
99
|
+
yield user_event_1.default.upload(input, file);
|
100
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
101
|
+
}));
|
102
|
+
it('should respect maxSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
103
|
+
const onAttachRejected = jest.fn();
|
104
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", maxSize: 100, onAttachRejected: onAttachRejected }));
|
105
|
+
const largeContent = 'x'.repeat(200);
|
106
|
+
const file = new File([largeContent], 'example.txt', { type: 'text/plain' });
|
107
|
+
const input = react_1.screen.getByTestId('input');
|
108
|
+
yield user_event_1.default.upload(input, file);
|
109
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
110
|
+
}));
|
111
|
+
it('should respect maxFiles restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
112
|
+
const onAttachRejected = jest.fn();
|
113
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", maxFiles: 1, onAttachRejected: onAttachRejected }));
|
114
|
+
const files = [
|
115
|
+
new File(['Test1'], 'example1.txt', { type: 'text/plain' }),
|
116
|
+
new File(['Test2'], 'example2.txt', { type: 'text/plain' })
|
117
|
+
];
|
118
|
+
const input = react_1.screen.getByTestId('input');
|
119
|
+
yield user_event_1.default.upload(input, files);
|
120
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
121
|
+
}));
|
122
|
+
it('should be disabled when isAttachmentDisabled is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
123
|
+
const onFileDrop = jest.fn();
|
124
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", isAttachmentDisabled: true }));
|
125
|
+
const file = new File(['Test'], 'example.text', { type: 'text/plain' });
|
126
|
+
const input = react_1.screen.getByTestId('input');
|
127
|
+
yield user_event_1.default.upload(input, file);
|
128
|
+
expect(onFileDrop).not.toHaveBeenCalled();
|
129
|
+
}));
|
130
|
+
it('should call onAttach when files are attached', () => __awaiter(void 0, void 0, void 0, function* () {
|
131
|
+
const onAttach = jest.fn();
|
132
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", onAttach: onAttach }));
|
133
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
134
|
+
const input = react_1.screen.getByTestId('input');
|
135
|
+
yield user_event_1.default.upload(input, file);
|
136
|
+
expect(onAttach).toHaveBeenCalled();
|
137
|
+
}));
|
138
|
+
it('should use custom validator when provided', () => __awaiter(void 0, void 0, void 0, function* () {
|
139
|
+
const validator = jest.fn().mockReturnValue({ message: 'Custom error' });
|
140
|
+
const onAttachRejected = jest.fn();
|
141
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", validator: validator, onAttachRejected: onAttachRejected }));
|
142
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
143
|
+
const input = react_1.screen.getByTestId('input');
|
144
|
+
yield user_event_1.default.upload(input, file);
|
145
|
+
expect(validator).toHaveBeenCalledWith(file);
|
146
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
147
|
+
}));
|
94
148
|
});
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import type { FunctionComponent } from 'react';
|
2
|
-
import { Accept } from 'react-dropzone/.';
|
2
|
+
import { Accept, DropzoneOptions, FileError, FileRejection } from 'react-dropzone/.';
|
3
3
|
import { ButtonProps, DropEvent, TextAreaProps, TooltipProps } from '@patternfly/react-core';
|
4
4
|
import { ChatbotDisplayMode } from '../Chatbot';
|
5
5
|
export interface MessageBarWithAttachMenuProps {
|
@@ -43,6 +43,28 @@ export interface MessageBarProps extends Omit<TextAreaProps, 'innerRef'> {
|
|
43
43
|
handleStopButton?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
44
44
|
/** Callback function for when attach button is used to upload a file */
|
45
45
|
handleAttach?: (data: File[], event: DropEvent) => void;
|
46
|
+
/** Specifies the file types accepted by the attachment upload component.
|
47
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
48
|
+
* For example,
|
49
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
50
|
+
**/
|
51
|
+
allowedFileTypes?: Accept;
|
52
|
+
/** Minimum file size allowed */
|
53
|
+
minSize?: number;
|
54
|
+
/** Max file size allowed */
|
55
|
+
maxSize?: number;
|
56
|
+
/** Max number of files allowed */
|
57
|
+
maxFiles?: number;
|
58
|
+
/** Whether attachments are disabled */
|
59
|
+
isAttachmentDisabled?: boolean;
|
60
|
+
/** Callback when file(s) are attached */
|
61
|
+
onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
|
62
|
+
/** Callback function for AttachButton when an attachment fails */
|
63
|
+
onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
|
64
|
+
/** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
|
65
|
+
validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
|
66
|
+
/** Additional props passed to react-dropzone */
|
67
|
+
dropzoneProps?: DropzoneOptions;
|
46
68
|
/** Props to enable a menu that opens when the Attach button is clicked, instead of the attachment window */
|
47
69
|
attachMenuProps?: MessageBarWithAttachMenuProps;
|
48
70
|
/** Flag to provide manual control over whether send button is disabled */
|
@@ -81,12 +103,6 @@ export interface MessageBarProps extends Omit<TextAreaProps, 'innerRef'> {
|
|
81
103
|
displayMode?: ChatbotDisplayMode;
|
82
104
|
/** Whether message bar is compact */
|
83
105
|
isCompact?: boolean;
|
84
|
-
/** Specifies the file types accepted by the attachment upload component.
|
85
|
-
* Files that don't match the accepted types will be disabled in the file picker.
|
86
|
-
* For example,
|
87
|
-
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
88
|
-
**/
|
89
|
-
allowedFileTypes?: Accept;
|
90
106
|
/** Ref applied to message bar textarea, for use with focus or other custom behaviors */
|
91
107
|
innerRef?: React.Ref<HTMLTextAreaElement>;
|
92
108
|
}
|
@@ -26,7 +26,7 @@ const AttachMenu_1 = __importDefault(require("../AttachMenu"));
|
|
26
26
|
const StopButton_1 = __importDefault(require("./StopButton"));
|
27
27
|
const MessageBarBase = (_a) => {
|
28
28
|
var _b;
|
29
|
-
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, innerRef } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact", "allowedFileTypes", "innerRef"]);
|
29
|
+
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, minSize, maxSize, maxFiles, isAttachmentDisabled, onAttach, onAttachRejected, validator, dropzoneProps, innerRef } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact", "allowedFileTypes", "minSize", "maxSize", "maxFiles", "isAttachmentDisabled", "onAttach", "onAttachRejected", "validator", "dropzoneProps", "innerRef"]);
|
30
30
|
// Text Input
|
31
31
|
// --------------------------------------------------------------------------
|
32
32
|
const [message, setMessage] = (0, react_1.useState)(value !== null && value !== void 0 ? value : '');
|
@@ -182,7 +182,7 @@ const MessageBarBase = (_a) => {
|
|
182
182
|
if (hasStopButton && handleStopButton) {
|
183
183
|
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)));
|
184
184
|
}
|
185
|
-
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)))] }));
|
185
|
+
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, minSize: minSize, maxSize: maxSize, maxFiles: maxFiles, isAttachmentDisabled: isAttachmentDisabled, onAttach: onAttach, onAttachRejected: onAttachRejected, validator: validator, dropzoneProps: dropzoneProps }, (_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, minSize: minSize, maxSize: maxSize, maxFiles: maxFiles, isAttachmentDisabled: isAttachmentDisabled, onAttach: onAttach, onAttachRejected: onAttachRejected, validator: validator, dropzoneProps: dropzoneProps }, (_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)))] }));
|
186
186
|
};
|
187
187
|
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() })] }));
|
188
188
|
if (attachMenuProps) {
|
@@ -1,7 +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
|
+
import { Accept, FileError, FileRejection } from 'react-dropzone/.';
|
5
5
|
export interface FileDropZoneProps {
|
6
6
|
/** Content displayed when the drop zone is not currently in use */
|
7
7
|
children?: React.ReactNode;
|
@@ -19,6 +19,20 @@ export interface FileDropZoneProps {
|
|
19
19
|
allowedFileTypes?: Accept;
|
20
20
|
/** Display mode for the Chatbot parent; this influences the styles applied */
|
21
21
|
displayMode?: ChatbotDisplayMode;
|
22
|
+
/** Minimum file size allowed */
|
23
|
+
minSize?: number;
|
24
|
+
/** Max file size allowed */
|
25
|
+
maxSize?: number;
|
26
|
+
/** Max number of files allowed */
|
27
|
+
maxFiles?: number;
|
28
|
+
/** Whether attachments are disabled */
|
29
|
+
isAttachmentDisabled?: boolean;
|
30
|
+
/** Callback when file(s) are attached */
|
31
|
+
onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
|
32
|
+
/** Callback function for AttachButton when an attachment fails */
|
33
|
+
onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
|
34
|
+
/** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
|
35
|
+
validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
|
22
36
|
}
|
23
37
|
declare const FileDropZone: FunctionComponent<FileDropZoneProps>;
|
24
38
|
export default FileDropZone;
|
@@ -15,9 +15,14 @@ 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, allowedFileTypes, displayMode = ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "allowedFileTypes", "displayMode"]);
|
18
|
+
var { children, className, infoText = 'Maximum file size is 25 MB', onFileDrop, allowedFileTypes, minSize, maxSize, maxFiles, isAttachmentDisabled, onAttach, onAttachRejected, validator, displayMode = ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "allowedFileTypes", "minSize", "maxSize", "maxFiles", "isAttachmentDisabled", "onAttach", "onAttachRejected", "validator", "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({ accept: allowedFileTypes, onDrop: (
|
21
|
+
return (_jsx(MultipleFileUpload, { dropzoneProps: Object.assign({ accept: allowedFileTypes, onDrop: (acceptedFiles, fileRejections, event) => {
|
22
|
+
setShowDropZone(false);
|
23
|
+
onAttach && onAttach(acceptedFiles, fileRejections, event);
|
24
|
+
}, minSize,
|
25
|
+
maxSize,
|
26
|
+
maxFiles, disabled: isAttachmentDisabled, onDropRejected: onAttachRejected, validator }, 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
27
|
};
|
23
28
|
export default FileDropZone;
|
@@ -37,4 +37,59 @@ describe('FileDropZone', () => {
|
|
37
37
|
yield userEvent.upload(fileInput, file);
|
38
38
|
expect(onFileDrop).not.toHaveBeenCalled();
|
39
39
|
}));
|
40
|
+
it('should respect minSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
41
|
+
const onAttachRejected = jest.fn();
|
42
|
+
const { container } = render(_jsx(FileDropZone, { onFileDrop: jest.fn(), minSize: 1000, onAttachRejected: onAttachRejected }));
|
43
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
44
|
+
const fileInput = container.querySelector('input[type="file"]');
|
45
|
+
yield userEvent.upload(fileInput, file);
|
46
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
47
|
+
}));
|
48
|
+
it('should respect maxSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
49
|
+
const onAttachRejected = jest.fn();
|
50
|
+
const { container } = render(_jsx(FileDropZone, { onFileDrop: jest.fn(), maxSize: 100, onAttachRejected: onAttachRejected }));
|
51
|
+
const largeContent = 'x'.repeat(200);
|
52
|
+
const file = new File([largeContent], 'example.txt', { type: 'text/plain' });
|
53
|
+
const fileInput = container.querySelector('input[type="file"]');
|
54
|
+
yield userEvent.upload(fileInput, file);
|
55
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
56
|
+
}));
|
57
|
+
it('should respect maxFiles restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
58
|
+
const onAttachRejected = jest.fn();
|
59
|
+
const { container } = render(_jsx(FileDropZone, { onFileDrop: jest.fn(), maxFiles: 1, onAttachRejected: onAttachRejected }));
|
60
|
+
const files = [
|
61
|
+
new File(['Test1'], 'example1.txt', { type: 'text/plain' }),
|
62
|
+
new File(['Test2'], 'example2.txt', { type: 'text/plain' })
|
63
|
+
];
|
64
|
+
const fileInput = container.querySelector('input[type="file"]');
|
65
|
+
yield userEvent.upload(fileInput, files);
|
66
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
67
|
+
}));
|
68
|
+
it('should be disabled when isAttachmentDisabled is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
69
|
+
const onFileDrop = jest.fn();
|
70
|
+
const { container } = render(_jsx(FileDropZone, { onFileDrop: onFileDrop, isAttachmentDisabled: true }));
|
71
|
+
const file = new File(['Test'], 'example.text', { type: 'text/plain' });
|
72
|
+
const fileInput = container.querySelector('input[type="file"]');
|
73
|
+
yield userEvent.upload(fileInput, file);
|
74
|
+
expect(onFileDrop).not.toHaveBeenCalled();
|
75
|
+
}));
|
76
|
+
it('should call onAttach when files are attached', () => __awaiter(void 0, void 0, void 0, function* () {
|
77
|
+
const onAttach = jest.fn();
|
78
|
+
const { container } = render(_jsx(FileDropZone, { onFileDrop: jest.fn(), onAttach: onAttach }));
|
79
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
80
|
+
const fileInput = container.querySelector('input[type="file"]');
|
81
|
+
yield userEvent.upload(fileInput, file);
|
82
|
+
expect(onAttach).toHaveBeenCalled();
|
83
|
+
}));
|
84
|
+
it('should use custom validator when provided', () => __awaiter(void 0, void 0, void 0, function* () {
|
85
|
+
const validator = jest.fn().mockReturnValue({ message: 'Custom error' });
|
86
|
+
const onAttachRejected = jest.fn();
|
87
|
+
const onFileDrop = jest.fn();
|
88
|
+
const { container } = render(_jsx(FileDropZone, { onFileDrop: onFileDrop, validator: validator, onAttachRejected: onAttachRejected }));
|
89
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
90
|
+
const fileInput = container.querySelector('input[type="file"]');
|
91
|
+
yield userEvent.upload(fileInput, file);
|
92
|
+
expect(validator).toHaveBeenCalledWith(file);
|
93
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
94
|
+
}));
|
40
95
|
});
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { ButtonProps, DropEvent, TooltipProps } from '@patternfly/react-core';
|
2
|
-
import { Accept } from 'react-dropzone';
|
2
|
+
import { Accept, DropzoneOptions, FileError, FileRejection } from 'react-dropzone';
|
3
3
|
export interface AttachButtonProps extends ButtonProps {
|
4
4
|
/** Callback for when button is clicked */
|
5
5
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
@@ -23,6 +23,23 @@ export interface AttachButtonProps extends ButtonProps {
|
|
23
23
|
tooltipContent?: string;
|
24
24
|
/** Test id applied to input */
|
25
25
|
inputTestId?: string;
|
26
|
+
/** Whether button is compact */
|
26
27
|
isCompact?: boolean;
|
28
|
+
/** Minimum file size allowed */
|
29
|
+
minSize?: number;
|
30
|
+
/** Max file size allowed */
|
31
|
+
maxSize?: number;
|
32
|
+
/** Max number of files allowed */
|
33
|
+
maxFiles?: number;
|
34
|
+
/** Whether attachments are disabled */
|
35
|
+
isAttachmentDisabled?: boolean;
|
36
|
+
/** Callback when file(s) are attached */
|
37
|
+
onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
|
38
|
+
/** Callback function for AttachButton when an attachment fails */
|
39
|
+
onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
|
40
|
+
/** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
|
41
|
+
validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
|
42
|
+
/** Additional props passed to react-dropzone */
|
43
|
+
dropzoneProps?: DropzoneOptions;
|
27
44
|
}
|
28
45
|
export declare const AttachButton: import("react").ForwardRefExoticComponent<AttachButtonProps & import("react").RefAttributes<any>>;
|
@@ -16,12 +16,10 @@ 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, allowedFileTypes } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact", "allowedFileTypes"]);
|
20
|
-
const { open, getInputProps } = useDropzone({
|
21
|
-
|
22
|
-
|
23
|
-
accept: allowedFileTypes
|
24
|
-
});
|
19
|
+
var { onAttachAccepted, onClick, isDisabled, className, tooltipProps, innerRef, tooltipContent = 'Attach', inputTestId, isCompact, allowedFileTypes, minSize, maxSize, maxFiles, isAttachmentDisabled, onAttach, onAttachRejected, validator, dropzoneProps } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact", "allowedFileTypes", "minSize", "maxSize", "maxFiles", "isAttachmentDisabled", "onAttach", "onAttachRejected", "validator", "dropzoneProps"]);
|
20
|
+
const { open, getInputProps } = useDropzone(Object.assign({ multiple: true, onDropAccepted: onAttachAccepted, accept: allowedFileTypes, minSize,
|
21
|
+
maxSize,
|
22
|
+
maxFiles, disabled: isAttachmentDisabled, onDrop: onAttach, onDropRejected: onAttachRejected, validator }, dropzoneProps));
|
25
23
|
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,
|
26
24
|
// prevents VO announcements of both aria label and tooltip
|
27
25
|
aria: "none" }, tooltipProps, { children: _jsx(Button, Object.assign({ variant: "plain", ref: innerRef, className: `pf-chatbot__button--attach ${isCompact ? 'pf-m-compact' : ''} ${className !== null && className !== void 0 ? className : ''}`, "aria-label": props['aria-label'] || 'Attach', isDisabled: isDisabled, onClick: onClick !== null && onClick !== void 0 ? onClick : open, icon: _jsx(Icon, { iconSize: isCompact ? 'lg' : 'xl', isInline: true, children: _jsx(PaperclipIcon, {}) }), size: isCompact ? 'sm' : undefined }, props)) }))] }));
|
@@ -86,4 +86,58 @@ describe('Attach button', () => {
|
|
86
86
|
yield userEvent.upload(input, file);
|
87
87
|
expect(onAttachAccepted).not.toHaveBeenCalled();
|
88
88
|
}));
|
89
|
+
it('should respect minSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
90
|
+
const onAttachRejected = jest.fn();
|
91
|
+
render(_jsx(AttachButton, { inputTestId: "input", minSize: 1000, onAttachRejected: onAttachRejected }));
|
92
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
93
|
+
const input = screen.getByTestId('input');
|
94
|
+
yield userEvent.upload(input, file);
|
95
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
96
|
+
}));
|
97
|
+
it('should respect maxSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
98
|
+
const onAttachRejected = jest.fn();
|
99
|
+
render(_jsx(AttachButton, { inputTestId: "input", maxSize: 100, onAttachRejected: onAttachRejected }));
|
100
|
+
const largeContent = 'x'.repeat(200);
|
101
|
+
const file = new File([largeContent], 'example.txt', { type: 'text/plain' });
|
102
|
+
const input = screen.getByTestId('input');
|
103
|
+
yield userEvent.upload(input, file);
|
104
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
105
|
+
}));
|
106
|
+
it('should respect maxFiles restriction', () => __awaiter(void 0, void 0, void 0, function* () {
|
107
|
+
const onAttachRejected = jest.fn();
|
108
|
+
render(_jsx(AttachButton, { inputTestId: "input", maxFiles: 1, onAttachRejected: onAttachRejected }));
|
109
|
+
const files = [
|
110
|
+
new File(['Test1'], 'example1.txt', { type: 'text/plain' }),
|
111
|
+
new File(['Test2'], 'example2.txt', { type: 'text/plain' })
|
112
|
+
];
|
113
|
+
const input = screen.getByTestId('input');
|
114
|
+
yield userEvent.upload(input, files);
|
115
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
116
|
+
}));
|
117
|
+
it('should be disabled when isAttachmentDisabled is true', () => __awaiter(void 0, void 0, void 0, function* () {
|
118
|
+
const onFileDrop = jest.fn();
|
119
|
+
render(_jsx(AttachButton, { inputTestId: "input", isAttachmentDisabled: true }));
|
120
|
+
const file = new File(['Test'], 'example.text', { type: 'text/plain' });
|
121
|
+
const input = screen.getByTestId('input');
|
122
|
+
yield userEvent.upload(input, file);
|
123
|
+
expect(onFileDrop).not.toHaveBeenCalled();
|
124
|
+
}));
|
125
|
+
it('should call onAttach when files are attached', () => __awaiter(void 0, void 0, void 0, function* () {
|
126
|
+
const onAttach = jest.fn();
|
127
|
+
render(_jsx(AttachButton, { inputTestId: "input", onAttach: onAttach }));
|
128
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
129
|
+
const input = screen.getByTestId('input');
|
130
|
+
yield userEvent.upload(input, file);
|
131
|
+
expect(onAttach).toHaveBeenCalled();
|
132
|
+
}));
|
133
|
+
it('should use custom validator when provided', () => __awaiter(void 0, void 0, void 0, function* () {
|
134
|
+
const validator = jest.fn().mockReturnValue({ message: 'Custom error' });
|
135
|
+
const onAttachRejected = jest.fn();
|
136
|
+
render(_jsx(AttachButton, { inputTestId: "input", validator: validator, onAttachRejected: onAttachRejected }));
|
137
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
138
|
+
const input = screen.getByTestId('input');
|
139
|
+
yield userEvent.upload(input, file);
|
140
|
+
expect(validator).toHaveBeenCalledWith(file);
|
141
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
142
|
+
}));
|
89
143
|
});
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import type { FunctionComponent } from 'react';
|
2
|
-
import { Accept } from 'react-dropzone/.';
|
2
|
+
import { Accept, DropzoneOptions, FileError, FileRejection } from 'react-dropzone/.';
|
3
3
|
import { ButtonProps, DropEvent, TextAreaProps, TooltipProps } from '@patternfly/react-core';
|
4
4
|
import { ChatbotDisplayMode } from '../Chatbot';
|
5
5
|
export interface MessageBarWithAttachMenuProps {
|
@@ -43,6 +43,28 @@ export interface MessageBarProps extends Omit<TextAreaProps, 'innerRef'> {
|
|
43
43
|
handleStopButton?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
44
44
|
/** Callback function for when attach button is used to upload a file */
|
45
45
|
handleAttach?: (data: File[], event: DropEvent) => void;
|
46
|
+
/** Specifies the file types accepted by the attachment upload component.
|
47
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
48
|
+
* For example,
|
49
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
50
|
+
**/
|
51
|
+
allowedFileTypes?: Accept;
|
52
|
+
/** Minimum file size allowed */
|
53
|
+
minSize?: number;
|
54
|
+
/** Max file size allowed */
|
55
|
+
maxSize?: number;
|
56
|
+
/** Max number of files allowed */
|
57
|
+
maxFiles?: number;
|
58
|
+
/** Whether attachments are disabled */
|
59
|
+
isAttachmentDisabled?: boolean;
|
60
|
+
/** Callback when file(s) are attached */
|
61
|
+
onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
|
62
|
+
/** Callback function for AttachButton when an attachment fails */
|
63
|
+
onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
|
64
|
+
/** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
|
65
|
+
validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
|
66
|
+
/** Additional props passed to react-dropzone */
|
67
|
+
dropzoneProps?: DropzoneOptions;
|
46
68
|
/** Props to enable a menu that opens when the Attach button is clicked, instead of the attachment window */
|
47
69
|
attachMenuProps?: MessageBarWithAttachMenuProps;
|
48
70
|
/** Flag to provide manual control over whether send button is disabled */
|
@@ -81,12 +103,6 @@ export interface MessageBarProps extends Omit<TextAreaProps, 'innerRef'> {
|
|
81
103
|
displayMode?: ChatbotDisplayMode;
|
82
104
|
/** Whether message bar is compact */
|
83
105
|
isCompact?: boolean;
|
84
|
-
/** Specifies the file types accepted by the attachment upload component.
|
85
|
-
* Files that don't match the accepted types will be disabled in the file picker.
|
86
|
-
* For example,
|
87
|
-
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
88
|
-
**/
|
89
|
-
allowedFileTypes?: Accept;
|
90
106
|
/** Ref applied to message bar textarea, for use with focus or other custom behaviors */
|
91
107
|
innerRef?: React.Ref<HTMLTextAreaElement>;
|
92
108
|
}
|
@@ -20,7 +20,7 @@ import AttachMenu from '../AttachMenu';
|
|
20
20
|
import StopButton from './StopButton';
|
21
21
|
export const MessageBarBase = (_a) => {
|
22
22
|
var _b;
|
23
|
-
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, innerRef } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact", "allowedFileTypes", "innerRef"]);
|
23
|
+
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, minSize, maxSize, maxFiles, isAttachmentDisabled, onAttach, onAttachRejected, validator, dropzoneProps, innerRef } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact", "allowedFileTypes", "minSize", "maxSize", "maxFiles", "isAttachmentDisabled", "onAttach", "onAttachRejected", "validator", "dropzoneProps", "innerRef"]);
|
24
24
|
// Text Input
|
25
25
|
// --------------------------------------------------------------------------
|
26
26
|
const [message, setMessage] = useState(value !== null && value !== void 0 ? value : '');
|
@@ -176,7 +176,7 @@ export const MessageBarBase = (_a) => {
|
|
176
176
|
if (hasStopButton && handleStopButton) {
|
177
177
|
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)));
|
178
178
|
}
|
179
|
-
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)))] }));
|
179
|
+
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, minSize: minSize, maxSize: maxSize, maxFiles: maxFiles, isAttachmentDisabled: isAttachmentDisabled, onAttach: onAttach, onAttachRejected: onAttachRejected, validator: validator, dropzoneProps: dropzoneProps }, (_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, minSize: minSize, maxSize: maxSize, maxFiles: maxFiles, isAttachmentDisabled: isAttachmentDisabled, onAttach: onAttach, onAttachRejected: onAttachRejected, validator: validator, dropzoneProps: dropzoneProps }, (_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)))] }));
|
180
180
|
};
|
181
181
|
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() })] }));
|
182
182
|
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.23",
|
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",
|
@@ -138,10 +138,17 @@ export const BasicDemo: FunctionComponent = () => {
|
|
138
138
|
}, 1000);
|
139
139
|
})
|
140
140
|
.catch((error: DOMException) => {
|
141
|
+
setShowAlert(true);
|
141
142
|
setError(`Failed to read file: ${error.message}`);
|
142
143
|
});
|
143
144
|
};
|
144
145
|
|
146
|
+
const handleAttachRejected = () => {
|
147
|
+
setFile(undefined);
|
148
|
+
setShowAlert(true);
|
149
|
+
setError('This demo only supports file extensions .txt, .json, .yaml, and .yaml. Please try a different file.');
|
150
|
+
};
|
151
|
+
|
145
152
|
const handleFileDrop = (event: DropEvent, data: File[]) => {
|
146
153
|
handleFile(data);
|
147
154
|
};
|
@@ -227,6 +234,7 @@ export const BasicDemo: FunctionComponent = () => {
|
|
227
234
|
'application/json': ['.json'],
|
228
235
|
'application/yaml': ['.yaml', '.yml']
|
229
236
|
}}
|
237
|
+
onAttachRejected={handleAttachRejected}
|
230
238
|
>
|
231
239
|
<ChatbotContent>
|
232
240
|
<MessageBox>
|
@@ -254,7 +262,17 @@ export const BasicDemo: FunctionComponent = () => {
|
|
254
262
|
<FileDetailsLabel fileName={file.name} isLoading={isLoadingFile} onClose={onClose} />
|
255
263
|
</div>
|
256
264
|
)}
|
257
|
-
<MessageBar
|
265
|
+
<MessageBar
|
266
|
+
onSendMessage={handleSend}
|
267
|
+
hasAttachButton
|
268
|
+
handleAttach={handleAttach}
|
269
|
+
allowedFileTypes={{
|
270
|
+
'text/plain': ['.txt'],
|
271
|
+
'application/json': ['.json'],
|
272
|
+
'application/yaml': ['.yaml', '.yml']
|
273
|
+
}}
|
274
|
+
onAttachRejected={handleAttachRejected}
|
275
|
+
/>
|
258
276
|
<ChatbotFootnote label="ChatBot uses AI. Check for mistakes." />
|
259
277
|
</ChatbotFooter>
|
260
278
|
</FileDropZone>
|
@@ -127,6 +127,7 @@ export const AttachmentMenuDemo: FunctionComponent = () => {
|
|
127
127
|
// Attachments
|
128
128
|
// --------------------------------------------------------------------------
|
129
129
|
const handleFileDrop = (event: DropEvent, data: File[]) => {
|
130
|
+
setIsOpen(false);
|
130
131
|
setFile(data[0]);
|
131
132
|
setIsLoadingFile(true);
|
132
133
|
setTimeout(() => {
|
@@ -40,4 +40,87 @@ describe('FileDropZone', () => {
|
|
40
40
|
|
41
41
|
expect(onFileDrop).not.toHaveBeenCalled();
|
42
42
|
});
|
43
|
+
|
44
|
+
it('should respect minSize restriction', async () => {
|
45
|
+
const onAttachRejected = jest.fn();
|
46
|
+
const { container } = render(
|
47
|
+
<FileDropZone onFileDrop={jest.fn()} minSize={1000} onAttachRejected={onAttachRejected} />
|
48
|
+
);
|
49
|
+
|
50
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
51
|
+
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
52
|
+
|
53
|
+
await userEvent.upload(fileInput, file);
|
54
|
+
|
55
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
56
|
+
});
|
57
|
+
it('should respect maxSize restriction', async () => {
|
58
|
+
const onAttachRejected = jest.fn();
|
59
|
+
const { container } = render(
|
60
|
+
<FileDropZone onFileDrop={jest.fn()} maxSize={100} onAttachRejected={onAttachRejected} />
|
61
|
+
);
|
62
|
+
|
63
|
+
const largeContent = 'x'.repeat(200);
|
64
|
+
const file = new File([largeContent], 'example.txt', { type: 'text/plain' });
|
65
|
+
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
66
|
+
|
67
|
+
await userEvent.upload(fileInput, file);
|
68
|
+
|
69
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
70
|
+
});
|
71
|
+
|
72
|
+
it('should respect maxFiles restriction', async () => {
|
73
|
+
const onAttachRejected = jest.fn();
|
74
|
+
const { container } = render(
|
75
|
+
<FileDropZone onFileDrop={jest.fn()} maxFiles={1} onAttachRejected={onAttachRejected} />
|
76
|
+
);
|
77
|
+
|
78
|
+
const files = [
|
79
|
+
new File(['Test1'], 'example1.txt', { type: 'text/plain' }),
|
80
|
+
new File(['Test2'], 'example2.txt', { type: 'text/plain' })
|
81
|
+
];
|
82
|
+
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
83
|
+
|
84
|
+
await userEvent.upload(fileInput, files);
|
85
|
+
|
86
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
87
|
+
});
|
88
|
+
|
89
|
+
it('should be disabled when isAttachmentDisabled is true', async () => {
|
90
|
+
const onFileDrop = jest.fn();
|
91
|
+
const { container } = render(<FileDropZone onFileDrop={onFileDrop} isAttachmentDisabled={true} />);
|
92
|
+
|
93
|
+
const file = new File(['Test'], 'example.text', { type: 'text/plain' });
|
94
|
+
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
95
|
+
await userEvent.upload(fileInput, file);
|
96
|
+
|
97
|
+
expect(onFileDrop).not.toHaveBeenCalled();
|
98
|
+
});
|
99
|
+
|
100
|
+
it('should call onAttach when files are attached', async () => {
|
101
|
+
const onAttach = jest.fn();
|
102
|
+
const { container } = render(<FileDropZone onFileDrop={jest.fn()} onAttach={onAttach} />);
|
103
|
+
|
104
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
105
|
+
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
106
|
+
|
107
|
+
await userEvent.upload(fileInput, file);
|
108
|
+
|
109
|
+
expect(onAttach).toHaveBeenCalled();
|
110
|
+
});
|
111
|
+
it('should use custom validator when provided', async () => {
|
112
|
+
const validator = jest.fn().mockReturnValue({ message: 'Custom error' });
|
113
|
+
const onAttachRejected = jest.fn();
|
114
|
+
const onFileDrop = jest.fn();
|
115
|
+
const { container } = render(
|
116
|
+
<FileDropZone onFileDrop={onFileDrop} validator={validator} onAttachRejected={onAttachRejected} />
|
117
|
+
);
|
118
|
+
|
119
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
120
|
+
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
121
|
+
await userEvent.upload(fileInput, file);
|
122
|
+
|
123
|
+
expect(validator).toHaveBeenCalledWith(file);
|
124
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
125
|
+
});
|
43
126
|
});
|
@@ -3,7 +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
|
+
import { Accept, FileError, FileRejection } from 'react-dropzone/.';
|
7
7
|
|
8
8
|
export interface FileDropZoneProps {
|
9
9
|
/** Content displayed when the drop zone is not currently in use */
|
@@ -22,6 +22,20 @@ export interface FileDropZoneProps {
|
|
22
22
|
allowedFileTypes?: Accept;
|
23
23
|
/** Display mode for the Chatbot parent; this influences the styles applied */
|
24
24
|
displayMode?: ChatbotDisplayMode;
|
25
|
+
/** Minimum file size allowed */
|
26
|
+
minSize?: number;
|
27
|
+
/** Max file size allowed */
|
28
|
+
maxSize?: number;
|
29
|
+
/** Max number of files allowed */
|
30
|
+
maxFiles?: number;
|
31
|
+
/** Whether attachments are disabled */
|
32
|
+
isAttachmentDisabled?: boolean;
|
33
|
+
/** Callback when file(s) are attached */
|
34
|
+
onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
|
35
|
+
/** Callback function for AttachButton when an attachment fails */
|
36
|
+
onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
|
37
|
+
/** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
|
38
|
+
validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
|
25
39
|
}
|
26
40
|
|
27
41
|
const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
|
@@ -30,6 +44,13 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
|
|
30
44
|
infoText = 'Maximum file size is 25 MB',
|
31
45
|
onFileDrop,
|
32
46
|
allowedFileTypes,
|
47
|
+
minSize,
|
48
|
+
maxSize,
|
49
|
+
maxFiles,
|
50
|
+
isAttachmentDisabled,
|
51
|
+
onAttach,
|
52
|
+
onAttachRejected,
|
53
|
+
validator,
|
33
54
|
displayMode = ChatbotDisplayMode.default,
|
34
55
|
...props
|
35
56
|
}: FileDropZoneProps) => {
|
@@ -50,7 +71,16 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
|
|
50
71
|
<MultipleFileUpload
|
51
72
|
dropzoneProps={{
|
52
73
|
accept: allowedFileTypes,
|
53
|
-
onDrop: () =>
|
74
|
+
onDrop: (acceptedFiles, fileRejections: FileRejection[], event: DropEvent) => {
|
75
|
+
setShowDropZone(false);
|
76
|
+
onAttach && onAttach(acceptedFiles, fileRejections, event);
|
77
|
+
},
|
78
|
+
minSize,
|
79
|
+
maxSize,
|
80
|
+
maxFiles,
|
81
|
+
disabled: isAttachmentDisabled,
|
82
|
+
onDropRejected: onAttachRejected,
|
83
|
+
validator,
|
54
84
|
...props
|
55
85
|
}}
|
56
86
|
onDragEnter={() => setShowDropZone(true)}
|
@@ -98,4 +98,79 @@ describe('Attach button', () => {
|
|
98
98
|
|
99
99
|
expect(onAttachAccepted).not.toHaveBeenCalled();
|
100
100
|
});
|
101
|
+
|
102
|
+
it('should respect minSize restriction', async () => {
|
103
|
+
const onAttachRejected = jest.fn();
|
104
|
+
render(<AttachButton inputTestId="input" minSize={1000} onAttachRejected={onAttachRejected} />);
|
105
|
+
|
106
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
107
|
+
const input = screen.getByTestId('input');
|
108
|
+
|
109
|
+
await userEvent.upload(input, file);
|
110
|
+
|
111
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
112
|
+
});
|
113
|
+
|
114
|
+
it('should respect maxSize restriction', async () => {
|
115
|
+
const onAttachRejected = jest.fn();
|
116
|
+
render(<AttachButton inputTestId="input" maxSize={100} onAttachRejected={onAttachRejected} />);
|
117
|
+
|
118
|
+
const largeContent = 'x'.repeat(200);
|
119
|
+
const file = new File([largeContent], 'example.txt', { type: 'text/plain' });
|
120
|
+
const input = screen.getByTestId('input');
|
121
|
+
|
122
|
+
await userEvent.upload(input, file);
|
123
|
+
|
124
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
125
|
+
});
|
126
|
+
|
127
|
+
it('should respect maxFiles restriction', async () => {
|
128
|
+
const onAttachRejected = jest.fn();
|
129
|
+
render(<AttachButton inputTestId="input" maxFiles={1} onAttachRejected={onAttachRejected} />);
|
130
|
+
|
131
|
+
const files = [
|
132
|
+
new File(['Test1'], 'example1.txt', { type: 'text/plain' }),
|
133
|
+
new File(['Test2'], 'example2.txt', { type: 'text/plain' })
|
134
|
+
];
|
135
|
+
|
136
|
+
const input = screen.getByTestId('input');
|
137
|
+
await userEvent.upload(input, files);
|
138
|
+
|
139
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
140
|
+
});
|
141
|
+
|
142
|
+
it('should be disabled when isAttachmentDisabled is true', async () => {
|
143
|
+
const onFileDrop = jest.fn();
|
144
|
+
render(<AttachButton inputTestId="input" isAttachmentDisabled={true} />);
|
145
|
+
|
146
|
+
const file = new File(['Test'], 'example.text', { type: 'text/plain' });
|
147
|
+
const input = screen.getByTestId('input');
|
148
|
+
await userEvent.upload(input, file);
|
149
|
+
|
150
|
+
expect(onFileDrop).not.toHaveBeenCalled();
|
151
|
+
});
|
152
|
+
|
153
|
+
it('should call onAttach when files are attached', async () => {
|
154
|
+
const onAttach = jest.fn();
|
155
|
+
render(<AttachButton inputTestId="input" onAttach={onAttach} />);
|
156
|
+
|
157
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
158
|
+
const input = screen.getByTestId('input');
|
159
|
+
|
160
|
+
await userEvent.upload(input, file);
|
161
|
+
|
162
|
+
expect(onAttach).toHaveBeenCalled();
|
163
|
+
});
|
164
|
+
it('should use custom validator when provided', async () => {
|
165
|
+
const validator = jest.fn().mockReturnValue({ message: 'Custom error' });
|
166
|
+
const onAttachRejected = jest.fn();
|
167
|
+
render(<AttachButton inputTestId="input" validator={validator} onAttachRejected={onAttachRejected} />);
|
168
|
+
|
169
|
+
const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
|
170
|
+
const input = screen.getByTestId('input');
|
171
|
+
await userEvent.upload(input, file);
|
172
|
+
|
173
|
+
expect(validator).toHaveBeenCalledWith(file);
|
174
|
+
expect(onAttachRejected).toHaveBeenCalled();
|
175
|
+
});
|
101
176
|
});
|
@@ -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 { Accept, useDropzone } from 'react-dropzone';
|
10
|
+
import { Accept, DropzoneOptions, FileError, FileRejection, 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 {
|
@@ -33,7 +33,24 @@ export interface AttachButtonProps extends ButtonProps {
|
|
33
33
|
tooltipContent?: string;
|
34
34
|
/** Test id applied to input */
|
35
35
|
inputTestId?: string;
|
36
|
+
/** Whether button is compact */
|
36
37
|
isCompact?: boolean;
|
38
|
+
/** Minimum file size allowed */
|
39
|
+
minSize?: number;
|
40
|
+
/** Max file size allowed */
|
41
|
+
maxSize?: number;
|
42
|
+
/** Max number of files allowed */
|
43
|
+
maxFiles?: number;
|
44
|
+
/** Whether attachments are disabled */
|
45
|
+
isAttachmentDisabled?: boolean;
|
46
|
+
/** Callback when file(s) are attached */
|
47
|
+
onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
|
48
|
+
/** Callback function for AttachButton when an attachment fails */
|
49
|
+
onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
|
50
|
+
/** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
|
51
|
+
validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
|
52
|
+
/** Additional props passed to react-dropzone */
|
53
|
+
dropzoneProps?: DropzoneOptions;
|
37
54
|
}
|
38
55
|
|
39
56
|
const AttachButtonBase: FunctionComponent<AttachButtonProps> = ({
|
@@ -47,12 +64,28 @@ const AttachButtonBase: FunctionComponent<AttachButtonProps> = ({
|
|
47
64
|
inputTestId,
|
48
65
|
isCompact,
|
49
66
|
allowedFileTypes,
|
67
|
+
minSize,
|
68
|
+
maxSize,
|
69
|
+
maxFiles,
|
70
|
+
isAttachmentDisabled,
|
71
|
+
onAttach,
|
72
|
+
onAttachRejected,
|
73
|
+
validator,
|
74
|
+
dropzoneProps,
|
50
75
|
...props
|
51
76
|
}: AttachButtonProps) => {
|
52
77
|
const { open, getInputProps } = useDropzone({
|
53
78
|
multiple: true,
|
54
79
|
onDropAccepted: onAttachAccepted,
|
55
|
-
accept: allowedFileTypes
|
80
|
+
accept: allowedFileTypes,
|
81
|
+
minSize,
|
82
|
+
maxSize,
|
83
|
+
maxFiles,
|
84
|
+
disabled: isAttachmentDisabled,
|
85
|
+
onDrop: onAttach,
|
86
|
+
onDropRejected: onAttachRejected,
|
87
|
+
validator,
|
88
|
+
...dropzoneProps
|
56
89
|
});
|
57
90
|
|
58
91
|
return (
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import type { ChangeEvent, FunctionComponent, KeyboardEvent as ReactKeyboardEvent, Ref } from 'react';
|
2
2
|
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
3
|
-
import { Accept } from 'react-dropzone/.';
|
3
|
+
import { Accept, DropzoneOptions, FileError, FileRejection } from 'react-dropzone/.';
|
4
4
|
import { ButtonProps, DropEvent, TextArea, TextAreaProps, TooltipProps } from '@patternfly/react-core';
|
5
5
|
|
6
6
|
// Import Chatbot components
|
@@ -53,6 +53,28 @@ export interface MessageBarProps extends Omit<TextAreaProps, 'innerRef'> {
|
|
53
53
|
handleStopButton?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
54
54
|
/** Callback function for when attach button is used to upload a file */
|
55
55
|
handleAttach?: (data: File[], event: DropEvent) => void;
|
56
|
+
/** Specifies the file types accepted by the attachment upload component.
|
57
|
+
* Files that don't match the accepted types will be disabled in the file picker.
|
58
|
+
* For example,
|
59
|
+
* allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
|
60
|
+
**/
|
61
|
+
allowedFileTypes?: Accept;
|
62
|
+
/** Minimum file size allowed */
|
63
|
+
minSize?: number;
|
64
|
+
/** Max file size allowed */
|
65
|
+
maxSize?: number;
|
66
|
+
/** Max number of files allowed */
|
67
|
+
maxFiles?: number;
|
68
|
+
/** Whether attachments are disabled */
|
69
|
+
isAttachmentDisabled?: boolean;
|
70
|
+
/** Callback when file(s) are attached */
|
71
|
+
onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
|
72
|
+
/** Callback function for AttachButton when an attachment fails */
|
73
|
+
onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
|
74
|
+
/** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
|
75
|
+
validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
|
76
|
+
/** Additional props passed to react-dropzone */
|
77
|
+
dropzoneProps?: DropzoneOptions;
|
56
78
|
/** Props to enable a menu that opens when the Attach button is clicked, instead of the attachment window */
|
57
79
|
attachMenuProps?: MessageBarWithAttachMenuProps;
|
58
80
|
/** Flag to provide manual control over whether send button is disabled */
|
@@ -80,12 +102,6 @@ export interface MessageBarProps extends Omit<TextAreaProps, 'innerRef'> {
|
|
80
102
|
displayMode?: ChatbotDisplayMode;
|
81
103
|
/** Whether message bar is compact */
|
82
104
|
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;
|
89
105
|
/** Ref applied to message bar textarea, for use with focus or other custom behaviors */
|
90
106
|
innerRef?: React.Ref<HTMLTextAreaElement>;
|
91
107
|
}
|
@@ -109,6 +125,14 @@ export const MessageBarBase: FunctionComponent<MessageBarProps> = ({
|
|
109
125
|
value,
|
110
126
|
isCompact = false,
|
111
127
|
allowedFileTypes,
|
128
|
+
minSize,
|
129
|
+
maxSize,
|
130
|
+
maxFiles,
|
131
|
+
isAttachmentDisabled,
|
132
|
+
onAttach,
|
133
|
+
onAttachRejected,
|
134
|
+
validator,
|
135
|
+
dropzoneProps,
|
112
136
|
innerRef,
|
113
137
|
...props
|
114
138
|
}: MessageBarProps) => {
|
@@ -309,6 +333,14 @@ export const MessageBarBase: FunctionComponent<MessageBarProps> = ({
|
|
309
333
|
isCompact={isCompact}
|
310
334
|
tooltipProps={buttonProps?.attach?.tooltipProps}
|
311
335
|
allowedFileTypes={allowedFileTypes}
|
336
|
+
minSize={minSize}
|
337
|
+
maxSize={maxSize}
|
338
|
+
maxFiles={maxFiles}
|
339
|
+
isAttachmentDisabled={isAttachmentDisabled}
|
340
|
+
onAttach={onAttach}
|
341
|
+
onAttachRejected={onAttachRejected}
|
342
|
+
validator={validator}
|
343
|
+
dropzoneProps={dropzoneProps}
|
312
344
|
{...buttonProps?.attach?.props}
|
313
345
|
/>
|
314
346
|
)}
|
@@ -321,6 +353,14 @@ export const MessageBarBase: FunctionComponent<MessageBarProps> = ({
|
|
321
353
|
isCompact={isCompact}
|
322
354
|
tooltipProps={buttonProps?.attach?.tooltipProps}
|
323
355
|
allowedFileTypes={allowedFileTypes}
|
356
|
+
minSize={minSize}
|
357
|
+
maxSize={maxSize}
|
358
|
+
maxFiles={maxFiles}
|
359
|
+
isAttachmentDisabled={isAttachmentDisabled}
|
360
|
+
onAttach={onAttach}
|
361
|
+
onAttachRejected={onAttachRejected}
|
362
|
+
validator={validator}
|
363
|
+
dropzoneProps={dropzoneProps}
|
324
364
|
{...buttonProps?.attach?.props}
|
325
365
|
/>
|
326
366
|
)}
|