Skip to content

Commit 58e22bd

Browse files
Merge pull request #575 from rebeccaalpert/onRejectHandler
fix(MessageBar/FileDropZone): Allow passing additional props to react-dropzone
2 parents 6f59d45 + 6deace2 commit 58e22bd

File tree

7 files changed

+292
-12
lines changed

7 files changed

+292
-12
lines changed

packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachment.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,17 @@ export const BasicDemo: FunctionComponent = () => {
138138
}, 1000);
139139
})
140140
.catch((error: DOMException) => {
141+
setShowAlert(true);
141142
setError(`Failed to read file: ${error.message}`);
142143
});
143144
};
144145

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+
145152
const handleFileDrop = (event: DropEvent, data: File[]) => {
146153
handleFile(data);
147154
};
@@ -227,6 +234,7 @@ export const BasicDemo: FunctionComponent = () => {
227234
'application/json': ['.json'],
228235
'application/yaml': ['.yaml', '.yml']
229236
}}
237+
onAttachRejected={handleAttachRejected}
230238
>
231239
<ChatbotContent>
232240
<MessageBox>
@@ -254,7 +262,17 @@ export const BasicDemo: FunctionComponent = () => {
254262
<FileDetailsLabel fileName={file.name} isLoading={isLoadingFile} onClose={onClose} />
255263
</div>
256264
)}
257-
<MessageBar onSendMessage={handleSend} hasAttachButton handleAttach={handleAttach} />
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+
/>
258276
<ChatbotFootnote label="ChatBot uses AI. Check for mistakes." />
259277
</ChatbotFooter>
260278
</FileDropZone>

packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachmentMenu.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export const AttachmentMenuDemo: FunctionComponent = () => {
127127
// Attachments
128128
// --------------------------------------------------------------------------
129129
const handleFileDrop = (event: DropEvent, data: File[]) => {
130+
setIsOpen(false);
130131
setFile(data[0]);
131132
setIsLoadingFile(true);
132133
setTimeout(() => {

packages/module/src/FileDropZone/FileDropZone.test.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,87 @@ describe('FileDropZone', () => {
4040

4141
expect(onFileDrop).not.toHaveBeenCalled();
4242
});
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+
});
43126
});

packages/module/src/FileDropZone/FileDropZone.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { FunctionComponent } from 'react';
33
import { useState } from 'react';
44
import { ChatbotDisplayMode } from '../Chatbot';
55
import { UploadIcon } from '@patternfly/react-icons';
6-
import { Accept } from 'react-dropzone/.';
6+
import { Accept, FileError, FileRejection } from 'react-dropzone/.';
77

88
export interface FileDropZoneProps {
99
/** Content displayed when the drop zone is not currently in use */
@@ -22,6 +22,20 @@ export interface FileDropZoneProps {
2222
allowedFileTypes?: Accept;
2323
/** Display mode for the Chatbot parent; this influences the styles applied */
2424
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;
2539
}
2640

2741
const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
@@ -30,6 +44,13 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
3044
infoText = 'Maximum file size is 25 MB',
3145
onFileDrop,
3246
allowedFileTypes,
47+
minSize,
48+
maxSize,
49+
maxFiles,
50+
isAttachmentDisabled,
51+
onAttach,
52+
onAttachRejected,
53+
validator,
3354
displayMode = ChatbotDisplayMode.default,
3455
...props
3556
}: FileDropZoneProps) => {
@@ -50,7 +71,16 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
5071
<MultipleFileUpload
5172
dropzoneProps={{
5273
accept: allowedFileTypes,
53-
onDrop: () => setShowDropZone(false),
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,
5484
...props
5585
}}
5686
onDragEnter={() => setShowDropZone(true)}

packages/module/src/MessageBar/AttachButton.test.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,79 @@ describe('Attach button', () => {
9898

9999
expect(onAttachAccepted).not.toHaveBeenCalled();
100100
});
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+
});
101176
});

packages/module/src/MessageBar/AttachButton.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { forwardRef } from 'react';
77

88
// Import PatternFly components
99
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';
1111
import { PaperclipIcon } from '@patternfly/react-icons/dist/esm/icons/paperclip-icon';
1212

1313
export interface AttachButtonProps extends ButtonProps {
@@ -33,7 +33,24 @@ export interface AttachButtonProps extends ButtonProps {
3333
tooltipContent?: string;
3434
/** Test id applied to input */
3535
inputTestId?: string;
36+
/** Whether button is compact */
3637
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;
3754
}
3855

3956
const AttachButtonBase: FunctionComponent<AttachButtonProps> = ({
@@ -47,12 +64,28 @@ const AttachButtonBase: FunctionComponent<AttachButtonProps> = ({
4764
inputTestId,
4865
isCompact,
4966
allowedFileTypes,
67+
minSize,
68+
maxSize,
69+
maxFiles,
70+
isAttachmentDisabled,
71+
onAttach,
72+
onAttachRejected,
73+
validator,
74+
dropzoneProps,
5075
...props
5176
}: AttachButtonProps) => {
5277
const { open, getInputProps } = useDropzone({
5378
multiple: true,
5479
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
5689
});
5790

5891
return (

0 commit comments

Comments
 (0)