Skip to content

Commit b4ceeb9

Browse files
committed
feat(History): Add expandable sections
Assisted-by: Cursor
1 parent ba585b9 commit b4ceeb9

5 files changed

Lines changed: 466 additions & 43 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { useState, FunctionComponent } from 'react';
2+
import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
3+
import ChatbotConversationHistoryNav, {
4+
Conversation,
5+
ConversationGroup
6+
} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
7+
import { Checkbox, MenuItem } from '@patternfly/react-core';
8+
9+
const pinnedChats: Conversation[] = [
10+
{
11+
id: '1',
12+
text: 'Red Hat products and services'
13+
}
14+
];
15+
16+
const recentChats: Conversation[] = [
17+
{
18+
id: '2',
19+
text: 'Enterprise Linux installation and setup'
20+
},
21+
{
22+
id: '3',
23+
text: 'Troubleshoot system crash'
24+
},
25+
{
26+
id: '4',
27+
text: 'Ansible security and updates'
28+
},
29+
{
30+
id: '5',
31+
text: 'Red Hat certification'
32+
},
33+
{
34+
id: '6',
35+
text: 'Lightspeed user documentation'
36+
}
37+
];
38+
39+
const savedPrompts: Conversation[] = [
40+
{
41+
id: '7',
42+
text: 'Summarize this document'
43+
},
44+
{
45+
id: '8',
46+
text: 'Draft a release announcement'
47+
}
48+
];
49+
50+
const VISIBLE_CHAT_COUNT = 3;
51+
52+
export const ChatbotHeaderDrawerWithCollapsibleGroupsDemo: FunctionComponent = () => {
53+
const [isOpen, setIsOpen] = useState(true);
54+
const [isSavedPromptsExpanded, setIsSavedPromptsExpanded] = useState(false);
55+
const [isShowingAllChats, setIsShowingAllChats] = useState(false);
56+
57+
const visibleChats = isShowingAllChats ? recentChats : recentChats.slice(0, VISIBLE_CHAT_COUNT);
58+
const hiddenChatCount = recentChats.length - VISIBLE_CHAT_COUNT;
59+
60+
const conversations: ConversationGroup[] = [
61+
{
62+
id: 'pinned',
63+
label: 'Pinned chats',
64+
items: pinnedChats
65+
},
66+
{
67+
id: 'chats',
68+
label: 'Chats',
69+
items: [
70+
...visibleChats,
71+
...(hiddenChatCount > 0 && !isShowingAllChats
72+
? [
73+
<MenuItem
74+
key="show-all-chats"
75+
itemId="show-all-chats"
76+
className="pf-chatbot__menu-item pf-chatbot__menu-item--show-all"
77+
onClick={() => setIsShowingAllChats(true)}
78+
>
79+
{`Show all (${recentChats.length})`}
80+
</MenuItem>
81+
]
82+
: [])
83+
]
84+
},
85+
{
86+
id: 'saved-prompts',
87+
label: 'Saved prompts',
88+
collapsible: {
89+
isExpanded: isSavedPromptsExpanded,
90+
onToggle: setIsSavedPromptsExpanded
91+
},
92+
items: savedPrompts
93+
}
94+
];
95+
96+
return (
97+
<>
98+
<Checkbox
99+
label="Display drawer"
100+
isChecked={isOpen}
101+
onChange={() => setIsOpen(!isOpen)}
102+
id="collapsible-groups-drawer-visible"
103+
name="collapsible-groups-drawer-visible"
104+
/>
105+
<ChatbotConversationHistoryNav
106+
displayMode={ChatbotDisplayMode.embedded}
107+
onDrawerToggle={() => setIsOpen(!isOpen)}
108+
isDrawerOpen={isOpen}
109+
setIsDrawerOpen={setIsOpen}
110+
conversations={conversations}
111+
drawerContent={<div>Drawer content</div>}
112+
/>
113+
</>
114+
);
115+
};

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ propComponents:
3535
'MessageBarWithAttachMenuProps',
3636
'SourceDetailsMenuItem',
3737
'ChatbotConversationHistoryNav',
38-
'Conversation'
38+
'Conversation',
39+
'ConversationGroup'
3940
]
4041
sortValue: 2
4142
---
@@ -415,6 +416,16 @@ To help users track important conversations, add a "pin" option to the conversat
415416

416417
```
417418

419+
### Grouped and collapsible conversation history
420+
421+
For grouped chat history, pass a `ConversationGroup[]` to describe each section's label, items, and behavior. Each group's `items` array accepts `Conversation` objects or custom menu content such as a **Show all** action. Use `collapsible` on a group to make that section expandable, and `footer` for content rendered after the group's menu list.
422+
423+
The object form `{ [groupLabel: string]: Conversation[] }` remains supported as shorthand for simple static groups.
424+
425+
```js file="./ChatbotHeaderDrawerWithCollapsibleGroups.tsx"
426+
427+
```
428+
418429
### Renaming conversations in chat history drawer
419430

420431
You can allow users to rename a conversation in the chat history drawer by implementing a modal that opens upon clicking a "Rename" (or similar) action. When doing so, you must ensure the following:

packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,56 @@
6060
--pf-v6-c-menu__item-main--ColumnGap: var(--pf-t--global--spacer--md);
6161
}
6262

63-
.pf-chatbot__menu-item-header > .pf-v6-c-menu__group-title {
64-
color: var(--pf-t--global--text--color--subtle);
65-
font-weight: var(--pf-t--global--font--weight--body--bold);
66-
font-size: var(--pf-t--global--icon--size--font--sm);
67-
--pf-v6-c-menu__group-title--PaddingInlineStart: var(--pf-t--global--spacer--sm);
68-
--pf-v6-c-menu__group-title--PaddingInlineEnd: var(--pf-t--global--spacer--sm);
63+
.pf-chatbot__menu-item-header > .pf-v6-c-menu__group-title,
64+
.pf-chatbot__menu-item-header--collapsible .pf-chatbot__menu-group-toggle {
6965
position: -webkit-sticky;
7066
position: sticky;
7167
top: 0;
7268
background-color: var(--pf-t--global--background--color--floating--default);
7369
z-index: var(--pf-t--global--z-index--md);
7470
}
7571

72+
.pf-chatbot__menu-item-header > .pf-v6-c-menu__group-title,
73+
.pf-chatbot__menu-item-header--collapsible .pf-chatbot__menu-group-toggle > .pf-v6-c-expandable-section__toggle {
74+
color: var(--pf-t--global--text--color--subtle);
75+
font-weight: var(--pf-t--global--font--weight--body--bold);
76+
font-size: var(--pf-t--global--icon--size--font--sm);
77+
}
78+
79+
.pf-chatbot__menu-group-toggle {
80+
padding-block-start: var(--pf-t--global--spacer--sm);
81+
padding-block-end: var(--pf-t--global--spacer--sm);
82+
}
83+
84+
.pf-chatbot__menu-item-header > .pf-v6-c-menu__group-title {
85+
--pf-v6-c-menu__group-title--PaddingInlineStart: var(--pf-t--global--spacer--sm);
86+
--pf-v6-c-menu__group-title--PaddingInlineEnd: var(--pf-t--global--spacer--sm);
87+
}
88+
89+
.pf-chatbot__menu-item-header--collapsible .pf-chatbot__menu-group-toggle {
90+
width: 100%;
91+
92+
.pf-v6-c-expandable-section__toggle {
93+
margin: 0;
94+
}
95+
96+
.pf-v6-c-button.pf-m-link {
97+
--pf-v6-c-button--PaddingBlockStart: 0;
98+
--pf-v6-c-button--PaddingBlockEnd: 0;
99+
--pf-v6-c-button--PaddingInlineStart: var(--pf-t--global--spacer--sm);
100+
--pf-v6-c-button--PaddingInlineEnd: var(--pf-t--global--spacer--sm);
101+
color: inherit;
102+
font-weight: inherit;
103+
font-size: inherit;
104+
justify-content: flex-start;
105+
width: 100%;
106+
}
107+
108+
.pf-v6-c-expandable-section__toggle-icon {
109+
color: var(--pf-t--global--text--color--subtle);
110+
}
111+
}
112+
76113
.pf-chatbot__menu-item {
77114
--pf-v6-c-menu__item--PaddingInlineStart: var(--pf-t--global--spacer--sm);
78115
--pf-v6-c-menu__item--PaddingInlineEnd: var(--pf-t--global--spacer--sm);
@@ -86,6 +123,11 @@
86123
border-radius: var(--pf-t--global--border--radius--small);
87124
}
88125

126+
.pf-chatbot__menu-item--show-all {
127+
color: var(--pf-t--global--text--color--link--default);
128+
font-size: var(--pf-t--global--font--size--body--default);
129+
}
130+
89131
li.pf-chatbot__menu-item:hover::after {
90132
position: absolute;
91133
inset: 0;

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

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import '@testing-library/jest-dom';
22
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
33

44
import { ChatbotDisplayMode } from '../Chatbot/Chatbot';
5-
import ChatbotConversationHistoryNav, { Conversation } from './ChatbotConversationHistoryNav';
6-
import { EmptyStateStatus, Spinner } from '@patternfly/react-core';
5+
import ChatbotConversationHistoryNav, { Conversation, ConversationGroup } from './ChatbotConversationHistoryNav';
6+
import { EmptyStateStatus, Spinner, MenuItem } from '@patternfly/react-core';
77
import { BellIcon, OutlinedCommentsIcon, SearchIcon } from '@patternfly/react-icons';
88
import { ComponentType } from 'react';
99

@@ -715,4 +715,145 @@ describe('ChatbotConversationHistoryNav', () => {
715715
);
716716
expect(screen.getByTestId('bell')).toBeInTheDocument();
717717
});
718+
719+
it('renders static and collapsible groups from ConversationGroup[]', () => {
720+
const groups: ConversationGroup[] = [
721+
{
722+
id: 'pinned',
723+
label: 'Pinned chats',
724+
items: initialConversations
725+
},
726+
{
727+
id: 'chats',
728+
label: 'Chats',
729+
collapsible: {
730+
isExpanded: true,
731+
onToggle: jest.fn()
732+
},
733+
items: [{ id: '2', text: 'Chatbot extension' }]
734+
}
735+
];
736+
737+
render(
738+
<ChatbotConversationHistoryNav
739+
onDrawerToggle={onDrawerToggle}
740+
isDrawerOpen={true}
741+
displayMode={ChatbotDisplayMode.fullscreen}
742+
setIsDrawerOpen={jest.fn()}
743+
conversations={groups}
744+
/>
745+
);
746+
747+
expect(screen.getByRole('heading', { name: 'Pinned chats', level: 3 })).toBeInTheDocument();
748+
expect(screen.queryByRole('button', { name: 'Pinned chats' })).not.toBeInTheDocument();
749+
expect(screen.getByRole('button', { name: 'Chats' })).toBeInTheDocument();
750+
});
751+
752+
it('collapses and expands a group when collapsible.onToggle is called', async () => {
753+
const onToggle = jest.fn();
754+
const groups: ConversationGroup[] = [
755+
{
756+
id: 'chats',
757+
label: 'Chats',
758+
collapsible: {
759+
isExpanded: true,
760+
onToggle
761+
},
762+
items: [{ id: '2', text: 'Chatbot extension' }]
763+
}
764+
];
765+
766+
const { rerender } = render(
767+
<ChatbotConversationHistoryNav
768+
onDrawerToggle={onDrawerToggle}
769+
isDrawerOpen={true}
770+
displayMode={ChatbotDisplayMode.fullscreen}
771+
setIsDrawerOpen={jest.fn()}
772+
conversations={groups}
773+
/>
774+
);
775+
776+
expect(screen.getByRole('menuitem', { name: /Chatbot extension/i })).toBeInTheDocument();
777+
778+
fireEvent.click(screen.getByRole('button', { name: 'Chats' }));
779+
expect(onToggle).toHaveBeenCalledWith(false);
780+
781+
rerender(
782+
<ChatbotConversationHistoryNav
783+
onDrawerToggle={onDrawerToggle}
784+
isDrawerOpen={true}
785+
displayMode={ChatbotDisplayMode.fullscreen}
786+
setIsDrawerOpen={jest.fn()}
787+
conversations={[
788+
{
789+
...groups[0],
790+
collapsible: {
791+
isExpanded: false,
792+
onToggle
793+
}
794+
}
795+
]}
796+
/>
797+
);
798+
799+
await waitFor(() => {
800+
expect(screen.queryByRole('menuitem', { name: /Chatbot extension/i })).not.toBeInTheDocument();
801+
});
802+
});
803+
804+
it('renders custom menu items and footers supplied in ConversationGroup', () => {
805+
const groups: ConversationGroup[] = [
806+
{
807+
id: 'chats',
808+
label: 'Chats',
809+
items: [
810+
initialConversations[0],
811+
<MenuItem key="show-all" itemId="show-all">
812+
Show all
813+
</MenuItem>
814+
],
815+
footer: <div data-testid="group-footer">Footer content</div>
816+
}
817+
];
818+
819+
render(
820+
<ChatbotConversationHistoryNav
821+
onDrawerToggle={onDrawerToggle}
822+
isDrawerOpen={true}
823+
displayMode={ChatbotDisplayMode.fullscreen}
824+
setIsDrawerOpen={jest.fn()}
825+
conversations={groups}
826+
/>
827+
);
828+
829+
expect(screen.getByRole('menuitem', { name: 'Show all' })).toBeInTheDocument();
830+
expect(screen.getByTestId('group-footer')).toBeInTheDocument();
831+
});
832+
833+
it('passes collapsible expandableSectionProps from ConversationGroup', () => {
834+
const groups: ConversationGroup[] = [
835+
{
836+
id: 'saved',
837+
label: 'Saved prompts',
838+
collapsible: {
839+
isExpanded: true,
840+
onToggle: jest.fn(),
841+
expandableSectionProps: { className: 'test-expandable-section' }
842+
},
843+
items: initialConversations
844+
}
845+
];
846+
847+
render(
848+
<ChatbotConversationHistoryNav
849+
onDrawerToggle={onDrawerToggle}
850+
isDrawerOpen={true}
851+
displayMode={ChatbotDisplayMode.fullscreen}
852+
setIsDrawerOpen={jest.fn()}
853+
conversations={groups}
854+
/>
855+
);
856+
857+
expect(document.querySelector('.test-expandable-section')).toBeInTheDocument();
858+
});
718859
});

0 commit comments

Comments
 (0)