Skip to content

Commit 63eaf1e

Browse files
authored
Merge pull request #445 from rebeccaalpert/loading-history
feat(ChatbotConversationHistoryNav): Add loading state and error state
2 parents 8d4dc9b + 0490acb commit 63eaf1e

File tree

6 files changed

+316
-22
lines changed

6 files changed

+316
-22
lines changed

packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawer.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
33
import ChatbotConversationHistoryNav, {
44
Conversation
55
} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
6-
import { Checkbox } from '@patternfly/react-core';
6+
import { Checkbox, EmptyStateStatus, Spinner } from '@patternfly/react-core';
77

88
const initialConversations: { [key: string]: Conversation[] } = {
99
Today: [{ id: '1', text: 'Red Hat products and services' }],
@@ -31,12 +31,29 @@ const initialConversations: { [key: string]: Conversation[] } = {
3131
]
3232
};
3333

34+
const ERROR = {
35+
bodyText: (
36+
<>
37+
To try again, check your connection and reload this page. If the issue persists,{' '}
38+
<a href="">contact the support team</a>.
39+
</>
40+
),
41+
buttonText: 'Reload',
42+
buttonIcon: <Spinner size="sm" />,
43+
hasButton: true,
44+
titleText: 'Could not load chat history',
45+
status: EmptyStateStatus.danger,
46+
onClick: () => alert('Clicked Reload')
47+
};
48+
3449
export const ChatbotHeaderTitleDemo: React.FunctionComponent = () => {
3550
const [isOpen, setIsOpen] = React.useState(true);
3651
const [isButtonOrderReversed, setIsButtonOrderReversed] = React.useState(false);
3752
const [conversations, setConversations] = React.useState<Conversation[] | { [key: string]: Conversation[] }>(
3853
initialConversations
3954
);
55+
const [isLoading, setIsLoading] = React.useState(false);
56+
const [hasError, setHasError] = React.useState(false);
4057
const displayMode = ChatbotDisplayMode.embedded;
4158

4259
const findMatchingItems = (targetValue: string) => {
@@ -74,6 +91,20 @@ export const ChatbotHeaderTitleDemo: React.FunctionComponent = () => {
7491
id="drawer-actions-visible"
7592
name="drawer-actions-visible"
7693
></Checkbox>
94+
<Checkbox
95+
label="Show loading state"
96+
isChecked={isLoading}
97+
onChange={() => setIsLoading(!isLoading)}
98+
id="drawer-is-loading"
99+
name="drawer-is-loading"
100+
></Checkbox>
101+
<Checkbox
102+
label="Show error state"
103+
isChecked={hasError}
104+
onChange={() => setHasError(!hasError)}
105+
id="drawer-has-error"
106+
name="drawer-has-error"
107+
></Checkbox>
77108
<ChatbotConversationHistoryNav
78109
displayMode={displayMode}
79110
onDrawerToggle={() => setIsOpen(!isOpen)}
@@ -96,6 +127,8 @@ export const ChatbotHeaderTitleDemo: React.FunctionComponent = () => {
96127
setConversations(newConversations);
97128
}}
98129
drawerContent={<div>Drawer content</div>}
130+
isLoading={isLoading}
131+
errorState={hasError ? ERROR : undefined}
99132
/>
100133
</>
101134
);

packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,17 @@
189189
}
190190
}
191191
}
192+
193+
.pf-chatbot__history-loading {
194+
display: flex;
195+
padding: var(--pf-t--global--spacer--lg);
196+
flex-direction: column;
197+
gap: var(--pf-t--global--spacer--lg);
198+
}
199+
200+
.pf-chatbot__history-loading-block {
201+
display: flex;
202+
flex-direction: column;
203+
gap: var(--pf-t--global--spacer--sm);
204+
align-self: stretch;
205+
}

packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,37 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
44

55
import { ChatbotDisplayMode } from '../Chatbot/Chatbot';
66
import ChatbotConversationHistoryNav, { Conversation } from './ChatbotConversationHistoryNav';
7+
import { EmptyStateStatus, Spinner } from '@patternfly/react-core';
8+
9+
const ERROR = {
10+
bodyText: (
11+
<>
12+
To try again, check your connection and reload this page. If the issue persists,{' '}
13+
<a href="">contact the support team</a>.
14+
</>
15+
),
16+
buttonText: 'Reload',
17+
buttonIcon: <Spinner size="sm" />,
18+
hasButton: true,
19+
titleText: 'Could not load chat history',
20+
status: EmptyStateStatus.danger,
21+
onClick: () => alert('Clicked Reload')
22+
};
23+
24+
const ERROR_WITHOUT_BUTTON = {
25+
bodyText: (
26+
<>
27+
To try again, check your connection and reload this page. If the issue persists,{' '}
28+
<a href="">contact the support team</a>.
29+
</>
30+
),
31+
buttonText: 'Reload',
32+
buttonIcon: <Spinner size="sm" />,
33+
hasButton: false,
34+
titleText: 'Could not load chat history',
35+
status: EmptyStateStatus.danger,
36+
onClick: () => alert('Clicked Reload')
37+
};
738

839
describe('ChatbotConversationHistoryNav', () => {
940
const onDrawerToggle = jest.fn();
@@ -232,4 +263,103 @@ describe('ChatbotConversationHistoryNav', () => {
232263
const element = container.querySelector('.test');
233264
expect(element).toBeInTheDocument();
234265
});
266+
267+
it('should show loading state if triggered', () => {
268+
render(
269+
<ChatbotConversationHistoryNav
270+
onDrawerToggle={onDrawerToggle}
271+
isDrawerOpen={true}
272+
displayMode={ChatbotDisplayMode.fullscreen}
273+
setIsDrawerOpen={jest.fn()}
274+
reverseButtonOrder={false}
275+
handleTextInputChange={jest.fn()}
276+
conversations={initialConversations}
277+
isLoading
278+
/>
279+
);
280+
expect(screen.getByRole('dialog', { name: /Loading chatbot conversation history/i })).toBeTruthy();
281+
expect(screen.getByRole('button', { name: /Close drawer panel/i })).toBeTruthy();
282+
});
283+
284+
it('should pass alternative aria label to loading state', () => {
285+
render(
286+
<ChatbotConversationHistoryNav
287+
onDrawerToggle={onDrawerToggle}
288+
isDrawerOpen={true}
289+
displayMode={ChatbotDisplayMode.fullscreen}
290+
setIsDrawerOpen={jest.fn()}
291+
reverseButtonOrder={false}
292+
handleTextInputChange={jest.fn()}
293+
conversations={initialConversations}
294+
isLoading
295+
loadingState={{ screenreaderText: 'I am a test' }}
296+
/>
297+
);
298+
expect(screen.getByRole('dialog', { name: /I am a test/i })).toBeTruthy();
299+
});
300+
301+
it('should accept errorState', () => {
302+
render(
303+
<ChatbotConversationHistoryNav
304+
onDrawerToggle={onDrawerToggle}
305+
isDrawerOpen={true}
306+
displayMode={ChatbotDisplayMode.fullscreen}
307+
setIsDrawerOpen={jest.fn()}
308+
reverseButtonOrder={false}
309+
handleTextInputChange={jest.fn()}
310+
conversations={initialConversations}
311+
errorState={ERROR}
312+
/>
313+
);
314+
expect(
315+
screen.getByRole('dialog', {
316+
name: /Could not load chat history To try again, check your connection and reload this page. If the issue persists, contact the support team . Loading... Reload/i
317+
})
318+
).toBeTruthy();
319+
expect(screen.getByRole('button', { name: /Close drawer panel/i })).toBeTruthy();
320+
expect(screen.getByRole('button', { name: /Loading... Reload/i })).toBeTruthy();
321+
expect(screen.getByRole('textbox', { name: /Filter menu items/i })).toBeTruthy();
322+
expect(screen.getByRole('heading', { name: /Could not load chat history/i })).toBeTruthy();
323+
});
324+
325+
it('should accept errorState without button', () => {
326+
render(
327+
<ChatbotConversationHistoryNav
328+
onDrawerToggle={onDrawerToggle}
329+
isDrawerOpen={true}
330+
displayMode={ChatbotDisplayMode.fullscreen}
331+
setIsDrawerOpen={jest.fn()}
332+
reverseButtonOrder={false}
333+
handleTextInputChange={jest.fn()}
334+
conversations={initialConversations}
335+
errorState={ERROR_WITHOUT_BUTTON}
336+
/>
337+
);
338+
expect(
339+
screen.getByRole('dialog', {
340+
name: /Could not load chat history To try again, check your connection and reload this page. If the issue persists, contact the support team ./i
341+
})
342+
).toBeTruthy();
343+
expect(screen.getByRole('button', { name: /Close drawer panel/i })).toBeTruthy();
344+
expect(screen.queryByRole('button', { name: /Loading... Reload/i })).toBeFalsy();
345+
expect(screen.getByRole('textbox', { name: /Filter menu items/i })).toBeTruthy();
346+
expect(screen.getByRole('heading', { name: /Could not load chat history/i })).toBeTruthy();
347+
});
348+
349+
it('should show loading state over error state if both are supplied', () => {
350+
render(
351+
<ChatbotConversationHistoryNav
352+
onDrawerToggle={onDrawerToggle}
353+
isDrawerOpen={true}
354+
displayMode={ChatbotDisplayMode.fullscreen}
355+
setIsDrawerOpen={jest.fn()}
356+
reverseButtonOrder={false}
357+
handleTextInputChange={jest.fn()}
358+
conversations={initialConversations}
359+
isLoading
360+
errorState={ERROR}
361+
/>
362+
);
363+
expect(screen.getByRole('dialog', { name: /Loading/i })).toBeTruthy();
364+
});
235365
});

packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@ import {
2929
DrawerHeadProps,
3030
DrawerActionsProps,
3131
DrawerCloseButtonProps,
32-
DrawerPanelBodyProps
32+
DrawerPanelBodyProps,
33+
SkeletonProps
3334
} from '@patternfly/react-core';
3435

3536
import { OutlinedCommentAltIcon } from '@patternfly/react-icons';
3637
import { ChatbotDisplayMode } from '../Chatbot/Chatbot';
3738
import ConversationHistoryDropdown from './ChatbotConversationHistoryDropdown';
39+
import LoadingState from './LoadingState';
40+
import HistoryEmptyState, { HistoryEmptyStateProps } from './EmptyState';
3841

3942
export interface Conversation {
4043
/** Conversation id */
@@ -103,6 +106,12 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps {
103106
drawerCloseButtonProps?: DrawerCloseButtonProps;
104107
/** Additional props appleid to drawer panel body */
105108
drawerPanelBodyProps?: DrawerPanelBodyProps;
109+
/** Whether to show drawer loading state */
110+
isLoading?: boolean;
111+
/** Additional props for loading state */
112+
loadingState?: SkeletonProps;
113+
/** Content to show in error state. Error state will appear once content is passed in. */
114+
errorState?: HistoryEmptyStateProps;
106115
}
107116

108117
export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConversationHistoryNavProps> = ({
@@ -129,6 +138,9 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
129138
drawerActionsProps,
130139
drawerCloseButtonProps,
131140
drawerPanelBodyProps,
141+
isLoading,
142+
loadingState,
143+
errorState,
132144
...props
133145
}: ChatbotConversationHistoryNavProps) => {
134146
const drawerRef = React.useRef<HTMLDivElement>(null);
@@ -194,24 +206,19 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
194206
// Menu Content
195207
// - Consumers should pass an array to <Chatbot> of the list of conversations
196208
// - Groups could be optional, but items need to be ordered by date
197-
const menuContent = (
198-
<Menu isPlain onSelect={onSelectActiveItem} activeItemId={activeItemId} {...menuProps}>
199-
<MenuContent>{buildMenu()}</MenuContent>
200-
</Menu>
201-
);
209+
const renderMenuContent = () => {
210+
if (errorState) {
211+
return <HistoryEmptyState {...errorState} />;
212+
}
213+
return (
214+
<Menu isPlain onSelect={onSelectActiveItem} activeItemId={activeItemId} {...menuProps}>
215+
<MenuContent>{buildMenu()}</MenuContent>
216+
</Menu>
217+
);
218+
};
202219

203-
const panelContent = (
204-
<DrawerPanelContent focusTrap={{ enabled: true }} defaultSize="384px" {...drawerPanelContentProps}>
205-
<DrawerHead {...drawerHeadProps}>
206-
<DrawerActions
207-
data-testid={drawerActionsTestId}
208-
className={reverseButtonOrder ? 'pf-v6-c-drawer__actions--reversed' : ''}
209-
{...drawerActionsProps}
210-
>
211-
<DrawerCloseButton onClick={onDrawerToggle} {...drawerCloseButtonProps} />
212-
{onNewChat && <Button onClick={onNewChat}>{newChatButtonText}</Button>}
213-
</DrawerActions>
214-
</DrawerHead>
220+
const renderDrawerContent = () => (
221+
<>
215222
{handleTextInputChange && (
216223
<div className="pf-chatbot__input">
217224
<SearchInput
@@ -221,10 +228,38 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
221228
/>
222229
</div>
223230
)}
224-
<DrawerPanelBody {...drawerPanelBodyProps}>{menuContent}</DrawerPanelBody>
225-
</DrawerPanelContent>
231+
<DrawerPanelBody {...drawerPanelBodyProps}>{renderMenuContent()}</DrawerPanelBody>
232+
</>
226233
);
227234

235+
const renderPanelContent = () => {
236+
const drawer = (
237+
<>
238+
<DrawerHead {...drawerHeadProps}>
239+
<DrawerActions
240+
data-testid={drawerActionsTestId}
241+
className={reverseButtonOrder ? 'pf-v6-c-drawer__actions--reversed' : ''}
242+
{...drawerActionsProps}
243+
>
244+
<DrawerCloseButton onClick={onDrawerToggle} {...drawerCloseButtonProps} />
245+
{onNewChat && <Button onClick={onNewChat}>{newChatButtonText}</Button>}
246+
</DrawerActions>
247+
</DrawerHead>
248+
{isLoading ? <LoadingState {...loadingState} /> : renderDrawerContent()}
249+
</>
250+
);
251+
return (
252+
<DrawerPanelContent
253+
aria-live="polite"
254+
focusTrap={{ enabled: true }}
255+
defaultSize="384px"
256+
{...drawerPanelContentProps}
257+
>
258+
{drawer}
259+
</DrawerPanelContent>
260+
);
261+
};
262+
228263
// An onKeyDown property must be passed to the Drawer component to handle closing
229264
// the drawer panel and deactivating the focus trap via the Escape key.
230265
const onEscape = (event: React.KeyboardEvent) => {
@@ -246,7 +281,7 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
246281
isInline={displayMode === ChatbotDisplayMode.fullscreen || displayMode === ChatbotDisplayMode.embedded}
247282
{...props}
248283
>
249-
<DrawerContent panelContent={panelContent} {...drawerContentProps}>
284+
<DrawerContent panelContent={renderPanelContent()} {...drawerContentProps}>
250285
<DrawerContentBody {...drawerContentBodyProps}>
251286
<>
252287
<div

0 commit comments

Comments
 (0)