@patternfly/chatbot 6.3.0-prerelease.22 → 6.3.0-prerelease.24
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/cjs/ResponseActions/ResponseActions.js +27 -2
- package/dist/cjs/ResponseActions/ResponseActions.test.js +60 -0
- 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/dist/esm/ResponseActions/ResponseActions.js +27 -2
- package/dist/esm/ResponseActions/ResponseActions.test.js +60 -0
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithClickedResponseActions.tsx +25 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomResponseActions.tsx +1 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +17 -0
- 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
- package/src/ResponseActions/ResponseActions.test.tsx +98 -1
- package/src/ResponseActions/ResponseActions.tsx +31 -2
@@ -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
|
)}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
2
2
|
import '@testing-library/jest-dom';
|
3
|
-
import ResponseActions from './ResponseActions';
|
3
|
+
import ResponseActions, { ActionProps } from './ResponseActions';
|
4
4
|
import userEvent from '@testing-library/user-event';
|
5
5
|
import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
|
6
6
|
import Message from '../Message';
|
@@ -129,6 +129,103 @@ describe('ResponseActions', () => {
|
|
129
129
|
expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
130
130
|
expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
131
131
|
});
|
132
|
+
|
133
|
+
it('should handle isClicked prop within group of buttons correctly', async () => {
|
134
|
+
render(
|
135
|
+
<ResponseActions
|
136
|
+
actions={
|
137
|
+
{
|
138
|
+
positive: { 'data-testid': 'positive-btn', onClick: jest.fn(), isClicked: true },
|
139
|
+
negative: { 'data-testid': 'negative-btn', onClick: jest.fn() }
|
140
|
+
} as Record<string, ActionProps>
|
141
|
+
}
|
142
|
+
/>
|
143
|
+
);
|
144
|
+
|
145
|
+
expect(screen.getByTestId('positive-btn')).toHaveClass('pf-chatbot__button--response-action-clicked');
|
146
|
+
expect(screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
147
|
+
});
|
148
|
+
|
149
|
+
it('should set "listen" button as active if its `isClicked` is true', async () => {
|
150
|
+
render(
|
151
|
+
<ResponseActions
|
152
|
+
actions={
|
153
|
+
{
|
154
|
+
positive: { 'data-testid': 'positive-btn', onClick: jest.fn(), isClicked: false },
|
155
|
+
negative: { 'data-testid': 'negative-btn', onClick: jest.fn(), isClicked: false },
|
156
|
+
listen: { 'data-testid': 'listen-btn', onClick: jest.fn(), isClicked: true }
|
157
|
+
} as Record<string, ActionProps>
|
158
|
+
}
|
159
|
+
/>
|
160
|
+
);
|
161
|
+
expect(screen.getByTestId('listen-btn')).toHaveClass('pf-chatbot__button--response-action-clicked');
|
162
|
+
|
163
|
+
expect(screen.getByTestId('positive-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
164
|
+
expect(screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
165
|
+
});
|
166
|
+
|
167
|
+
it('should prioritize "positive" when both "positive" and "negative" are set to clicked', async () => {
|
168
|
+
render(
|
169
|
+
<ResponseActions
|
170
|
+
actions={
|
171
|
+
{
|
172
|
+
positive: { 'data-testid': 'positive-btn', onClick: jest.fn(), isClicked: true },
|
173
|
+
negative: { 'data-testid': 'negative-btn', onClick: jest.fn(), isClicked: true }
|
174
|
+
} as Record<string, ActionProps>
|
175
|
+
}
|
176
|
+
/>
|
177
|
+
);
|
178
|
+
// Positive button should take precendence
|
179
|
+
expect(screen.getByTestId('positive-btn')).toHaveClass('pf-chatbot__button--response-action-clicked');
|
180
|
+
expect(screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
181
|
+
});
|
182
|
+
|
183
|
+
it('should set an additional action button as active if it is initially clicked and no predefined are clicked', async () => {
|
184
|
+
const [additionalActions] = CUSTOM_ACTIONS;
|
185
|
+
const customActions = {
|
186
|
+
positive: { 'data-testid': 'positive', onClick: jest.fn(), isClicked: false },
|
187
|
+
negative: { 'data-testid': 'negative', onClick: jest.fn(), isClicked: false },
|
188
|
+
...Object.keys(additionalActions).reduce((acc, actionKey) => {
|
189
|
+
acc[actionKey] = {
|
190
|
+
...additionalActions[actionKey],
|
191
|
+
'data-testid': actionKey,
|
192
|
+
isClicked: actionKey === 'regenerate'
|
193
|
+
};
|
194
|
+
return acc;
|
195
|
+
}, {})
|
196
|
+
};
|
197
|
+
render(<ResponseActions actions={customActions} />);
|
198
|
+
|
199
|
+
Object.keys(customActions).forEach((actionKey) => {
|
200
|
+
if (actionKey === 'regenerate') {
|
201
|
+
expect(screen.getByTestId(actionKey)).toHaveClass('pf-chatbot__button--response-action-clicked');
|
202
|
+
} else {
|
203
|
+
// Other actions should not have clicked class
|
204
|
+
expect(screen.getByTestId(actionKey)).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
205
|
+
}
|
206
|
+
});
|
207
|
+
});
|
208
|
+
|
209
|
+
it('should activate the clicked button and deactivate any previously active button', async () => {
|
210
|
+
const actions = {
|
211
|
+
positive: { 'data-testid': 'positive', onClick: jest.fn(), isClicked: false },
|
212
|
+
negative: { 'data-testid': 'negative', onClick: jest.fn(), isClicked: true }
|
213
|
+
};
|
214
|
+
render(<ResponseActions actions={actions} />);
|
215
|
+
|
216
|
+
const negativeBtn = screen.getByTestId('negative');
|
217
|
+
const positiveBtn = screen.getByTestId('positive');
|
218
|
+
// negative button is initially clicked
|
219
|
+
expect(negativeBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
|
220
|
+
expect(positiveBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
221
|
+
|
222
|
+
await userEvent.click(positiveBtn);
|
223
|
+
|
224
|
+
// positive button should now have the clicked class
|
225
|
+
expect(positiveBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
|
226
|
+
expect(negativeBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
227
|
+
});
|
228
|
+
|
132
229
|
it('should render buttons correctly', () => {
|
133
230
|
ALL_ACTIONS.forEach(({ type, label }) => {
|
134
231
|
render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
|
@@ -55,12 +55,40 @@ export interface ResponseActionProps {
|
|
55
55
|
|
56
56
|
export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ actions }) => {
|
57
57
|
const [activeButton, setActiveButton] = useState<string>();
|
58
|
+
const [clickStatePersisted, setClickStatePersisted] = useState<boolean>(false);
|
59
|
+
useEffect(() => {
|
60
|
+
// Define the order of precedence for checking initial `isClicked`
|
61
|
+
const actionPrecedence = ['positive', 'negative', 'copy', 'share', 'download', 'listen'];
|
62
|
+
let initialActive: string | undefined;
|
63
|
+
|
64
|
+
// Check predefined actions first based on precedence
|
65
|
+
for (const actionName of actionPrecedence) {
|
66
|
+
const actionProp = actions[actionName as keyof typeof actions];
|
67
|
+
if (actionProp?.isClicked) {
|
68
|
+
initialActive = actionName;
|
69
|
+
break;
|
70
|
+
}
|
71
|
+
}
|
72
|
+
// If no predefined action was initially clicked, check additionalActions
|
73
|
+
if (!initialActive) {
|
74
|
+
const clickedActionName = Object.keys(additionalActions).find(
|
75
|
+
(actionName) => !actionPrecedence.includes(actionName) && additionalActions[actionName]?.isClicked
|
76
|
+
);
|
77
|
+
initialActive = clickedActionName;
|
78
|
+
}
|
79
|
+
if (initialActive) {
|
80
|
+
// Click state is explicitly controlled by consumer.
|
81
|
+
setClickStatePersisted(true);
|
82
|
+
}
|
83
|
+
setActiveButton(initialActive);
|
84
|
+
}, [actions]);
|
85
|
+
|
58
86
|
const { positive, negative, copy, share, download, listen, ...additionalActions } = actions;
|
59
87
|
const responseActions = useRef<HTMLDivElement>(null);
|
60
88
|
|
61
89
|
useEffect(() => {
|
62
90
|
const handleClickOutside = (e) => {
|
63
|
-
if (responseActions.current && !responseActions.current.contains(e.target)) {
|
91
|
+
if (responseActions.current && !responseActions.current.contains(e.target) && !clickStatePersisted) {
|
64
92
|
setActiveButton(undefined);
|
65
93
|
}
|
66
94
|
};
|
@@ -69,13 +97,14 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ action
|
|
69
97
|
return () => {
|
70
98
|
window.removeEventListener('click', handleClickOutside);
|
71
99
|
};
|
72
|
-
}, []);
|
100
|
+
}, [clickStatePersisted]);
|
73
101
|
|
74
102
|
const handleClick = (
|
75
103
|
e: MouseEvent | MouseEvent<Element, MouseEvent> | KeyboardEvent,
|
76
104
|
id: string,
|
77
105
|
onClick?: (event: MouseEvent | MouseEvent<Element, MouseEvent> | KeyboardEvent) => void
|
78
106
|
) => {
|
107
|
+
setClickStatePersisted(false);
|
79
108
|
setActiveButton(id);
|
80
109
|
onClick && onClick(e);
|
81
110
|
};
|