From d8d26bafda3587a0221534435752a56de5b21065 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 25 Nov 2025 13:57:32 -0500 Subject: [PATCH 1/9] feat(ConversationHistoryNav): added search actions --- .../ChatbotHeaderDrawerWithSearchActions.tsx | 179 ++++++++++++++++++ .../extensions/chatbot/examples/UI/UI.md | 16 +- .../ChatbotConversationHistoryNav.scss | 11 ++ .../ChatbotConversationHistoryNav.test.tsx | 68 +++++++ .../ChatbotConversationHistoryNav.tsx | 56 ++++-- 5 files changed, 312 insertions(+), 18 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx new file mode 100644 index 000000000..0545606ef --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx @@ -0,0 +1,179 @@ +import { FunctionComponent, useState } from 'react'; +import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot'; +import ChatbotConversationHistoryNav, { + Conversation +} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; +import { + Button, + Checkbox, + MenuToggle, + MenuToggleElement, + Select, + SelectList, + SelectOption +} from '@patternfly/react-core'; +import { FilterIcon, SortAmountDownIcon } from '@patternfly/react-icons'; + +const initialConversations: { [key: string]: Conversation[] } = { + Today: [{ id: '1', text: 'Red Hat products and services' }], + 'This month': [ + { + id: '2', + text: 'Enterprise Linux installation and setup' + }, + { id: '3', text: 'Troubleshoot system crash' } + ], + March: [ + { id: '4', text: 'Ansible security and updates' }, + { id: '5', text: 'Red Hat certification' }, + { id: '6', text: 'Lightspeed user documentation' } + ], + February: [ + { id: '7', text: 'Crashing pod assistance' }, + { id: '8', text: 'OpenShift AI pipelines' }, + { id: '9', text: 'Updating subscription plan' }, + { id: '10', text: 'Red Hat licensing options' } + ], + January: [ + { id: '11', text: 'RHEL system performance' }, + { id: '12', text: 'Manage user accounts' } + ] +}; + +export const ChatbotHeaderTitleDemo: FunctionComponent = () => { + const [isDrawerOpen, setIsDrawerOpen] = useState(true); + const [hasDrawerHeadDivider, setHasDrawerHeadDivider] = useState(false); + const [showSearchActionStart, setShowSearchActionStart] = useState(false); + const [showSearchActionEnd, setShowSearchActionEnd] = useState(false); + const [isSortSelectOpen, setIsSortSelectOpen] = useState(false); + const [selectedSort, setSelectedSort] = useState('newest'); + const [conversations, setConversations] = useState( + initialConversations + ); + const displayMode = ChatbotDisplayMode.embedded; + + const sortLabels: { [key: string]: string } = { + newest: 'Date (newest first)', + oldest: 'Date (oldest first)', + 'alphabetical-asc': 'Name (A-Z)', + 'alphabetical-desc': 'Name (Z-A)' + }; + + const onSortSelect = (_event: React.MouseEvent | undefined, value: string | number | undefined) => { + setSelectedSort(value as string); + setIsSortSelectOpen(false); + }; + + const findMatchingItems = (targetValue: string) => { + const filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => { + const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase())); + if (filteredItems.length > 0) { + acc[key] = filteredItems; + } + return acc; + }, {}); + + return filteredConversations; + }; + + return ( + <> + setIsDrawerOpen(!isDrawerOpen)} + id="drawer-visible" + name="drawer-visible" + /> + setHasDrawerHeadDivider(!hasDrawerHeadDivider)} + id="drawer-head-divider" + name="drawer-head-divider" + /> + setShowSearchActionStart(!showSearchActionStart)} + id="show-search-action-start" + name="show-search-action-start" + /> + setShowSearchActionEnd(!showSearchActionEnd)} + id="show-search-action-end" + name="show-search-action-end" + /> + setIsDrawerOpen(!isDrawerOpen)} + isDrawerOpen={isDrawerOpen} + setIsDrawerOpen={setIsDrawerOpen} + // eslint-disable-next-line no-console + onSelectActiveItem={(e, selectedItem) => console.log(`Selected history item with id ${selectedItem}`)} + conversations={conversations} + onNewChat={() => { + setIsDrawerOpen(!isDrawerOpen); + }} + handleTextInputChange={(value: string) => { + if (value === '') { + setConversations(initialConversations); + } else { + const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value); + setConversations(newConversations); + } + }} + drawerContent={
Drawer content
} + hasDrawerHeadDivider={hasDrawerHeadDivider} + searchActionStart={ + showSearchActionStart ? ( + + ) : undefined + } + searchActionEnd={ + showSearchActionEnd ? ( + + ) : undefined + } + /> + + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md index d636daa1b..8c7f8625d 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md @@ -73,8 +73,8 @@ import SettingsForm from '@patternfly/chatbot/dist/dynamic/Settings'; import { BellIcon, CalendarAltIcon, ClipboardIcon, CodeIcon, ThumbtackIcon, UploadIcon } from '@patternfly/react-icons'; import { useDropzone } from 'react-dropzone'; -import ChatbotConversationHistoryNav from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; -import { DropdownItem, DropdownList, Checkbox } from '@patternfly/react-core'; +import ChatbotConversationHistoryNav, { Conversation } from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; +import { Button, DropdownItem, DropdownList, Checkbox, MenuToggle, MenuToggleElement, Select, SelectList, SelectOption } from '@patternfly/react-core'; import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon'; import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon'; @@ -87,7 +87,7 @@ import userAvatar from '../Messages/user_avatar.svg'; import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; import termsAndConditionsHeader from './PF-TermsAndConditionsHeader.svg'; import onboardingHeader from './RH-Hat-Image.svg'; -import { CloseIcon, SearchIcon, OutlinedCommentsIcon } from '@patternfly/react-icons'; +import { CloseIcon, SearchIcon, OutlinedCommentsIcon, FilterIcon, SortAmountDownIcon } from '@patternfly/react-icons'; import { FunctionComponent, FormEvent, useState, useRef, MouseEvent, isValidElement, cloneElement, Children, ReactNode, Ref, MouseEvent as ReactMouseEvent, CSSProperties, useEffect} from 'react'; import FilePreview from '@patternfly/chatbot/dist/dynamic/FilePreview'; @@ -371,6 +371,16 @@ Both the search input field and "New chat" buttons are optional. The `reverseBut ``` +### Drawer with search actions + +The conversation history drawer supports additional customization through `searchActionStart` and `searchActionEnd` props, which allow you to add controls before and after the search input field. These props are useful for adding filtering, sorting, or other search-related functionality. + +You can also add a visual divider between the drawer head and the title by setting `hasDrawerHeadDivider` to `true`. + +```ts file="./ChatbotHeaderDrawerWithSearchActions.tsx" + +``` + ### Drawer with conversation actions Actions can be added to conversations with `menuItems`. Optionally, you can also add a `className` to the menu via `menuClassName`, change the default aria-label and tooltip content via `label`, or add an `onSelect` callback for when a user selects an item. diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss index d632bed9f..24bda4eee 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss @@ -7,6 +7,11 @@ border-radius: var(--pf-t--global--border--radius--medium); } + .pf-chatbot__heading-divider { + padding-inline-start: var(--pf-t--global--spacer--lg); + padding-inline-end: var(--pf-t--global--spacer--lg); + } + // Drawer title // ---------------------------------------------------------------------------- .pf-chatbot__heading-container { @@ -28,6 +33,12 @@ justify-content: flex-start; gap: var(--pf-t--global--spacer--gap--text-to-element--default); } + + // Drawer search and actions + .pf-chatbot__input { + width: 100%; + } + // Drawer menu // ---------------------------------------------------------------------------- .pf-v6-c-menu { diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx index f1c21c6ce..14a10e10e 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx @@ -592,6 +592,74 @@ describe('ChatbotConversationHistoryNav', () => { expect(screen.getByRole('dialog', { name: /Chat history I am a sample search/i })).toBeInTheDocument(); }); + it('Does not render search actions by default', () => { + const handleSearch = jest.fn(); + const groupedConversations: { [key: string]: Conversation[] } = { + Today: [...initialConversations, { id: '2', text: 'Chatbot extension' }] + }; + + render( + + ); + + const searchInput = screen.getByPlaceholderText(/Search/i); + + expect(searchInput.parentElement?.previousElementSibling).toBeNull(); + expect(searchInput.parentElement?.nextElementSibling).toBeNull(); + }) + + it('Renders with action at start when searchActionStart is passed', () => { + const handleSearch = jest.fn(); + const groupedConversations: { [key: string]: Conversation[] } = { + Today: [...initialConversations, { id: '2', text: 'Chatbot extension' }] + }; + + render( + Search action start test} + /> + ); + + expect(screen.getByText("Search action start test")).toBeVisible(); + }) + + it('Renders with action at end when searchActionEnd is passed', () => { + const handleSearch = jest.fn(); + const groupedConversations: { [key: string]: Conversation[] } = { + Today: [...initialConversations, { id: '2', text: 'Chatbot extension' }] + }; + + render( + Search action end test} + /> + ); + + expect(screen.getByText("Search action end test")).toBeVisible(); + }) + it('overrides nav title heading level when navTitleProps.headingLevel is passed', () => { render( ; /** Visually hidden text that gets announced by assistive technologies. Should be used to convey the result count when the search input value changes. */ searchInputScreenReaderText?: string; + /** Custom action rendered before the search input. */ + searchActionStart?: React.ReactNode; + /** Custom action rendered after the search input. */ + searchActionEnd?: React.ReactNode; /** Additional props passed to MenuContent */ menuContentProps?: Omit; } @@ -175,6 +184,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent, searchInputScreenReaderText, + searchActionStart, + searchActionEnd, menuProps, menuGroupProps, menuContentProps, @@ -287,6 +299,31 @@ export const ChatbotConversationHistoryNav: FunctionComponent ); + const searchInputContainer = handleTextInputChange && ( +
+ handleTextInputChange(value)} + placeholder={searchInputPlaceholder} + {...searchInputProps} + /> + {searchInputScreenReaderText && ( +
{searchInputScreenReaderText}
+ )} +
+ ); + + const renderSearchAndActions = () => + searchActionStart || searchActionEnd ? ( + + {searchActionStart && {searchActionStart}} + {searchInputContainer && {searchInputContainer}} + {searchActionEnd && {searchActionEnd}} + + ) : ( + searchInputContainer + ); + const renderPanelContent = () => { const drawer = ( <> @@ -309,6 +346,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent + {hasDrawerHeadDivider && }
@@ -318,19 +356,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent
- {!isLoading && handleTextInputChange && ( -
- handleTextInputChange(value)} - placeholder={searchInputPlaceholder} - {...searchInputProps} - /> - {searchInputScreenReaderText && ( -
{searchInputScreenReaderText}
- )} -
- )} + {!isLoading && renderSearchAndActions()}
{isLoading ? : renderDrawerContent()} From 969ce1a4cfe701a7d7687eeb14d1fdb76188d1c3 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 25 Nov 2025 14:09:05 -0500 Subject: [PATCH 2/9] Updated icon style for ascending --- .../ChatbotHeaderDrawerWithSearchActions.tsx | 20 +++++++++---------- .../ChatbotConversationHistoryNav.test.tsx | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx index 0545606ef..2adec2287 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx @@ -134,9 +134,8 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => { aria-label="Filter options" // eslint-disable-next-line no-console onClick={() => console.log('Filter button clicked')} - > - - + icon={} + /> ) : undefined } searchActionEnd={ @@ -154,13 +153,14 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => { isExpanded={isSortSelectOpen} variant="plain" aria-label={`${sortLabels[selectedSort]}, Sort conversations`} - > - - + icon={ + + } + /> )} > diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx index 14a10e10e..c8d2fc974 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx @@ -614,7 +614,7 @@ describe('ChatbotConversationHistoryNav', () => { expect(searchInput.parentElement?.previousElementSibling).toBeNull(); expect(searchInput.parentElement?.nextElementSibling).toBeNull(); - }) + }); it('Renders with action at start when searchActionStart is passed', () => { const handleSearch = jest.fn(); @@ -635,8 +635,8 @@ describe('ChatbotConversationHistoryNav', () => { /> ); - expect(screen.getByText("Search action start test")).toBeVisible(); - }) + expect(screen.getByText('Search action start test')).toBeVisible(); + }); it('Renders with action at end when searchActionEnd is passed', () => { const handleSearch = jest.fn(); @@ -657,8 +657,8 @@ describe('ChatbotConversationHistoryNav', () => { /> ); - expect(screen.getByText("Search action end test")).toBeVisible(); - }) + expect(screen.getByText('Search action end test')).toBeVisible(); + }); it('overrides nav title heading level when navTitleProps.headingLevel is passed', () => { render( From d1975054b5ad1f6af90625969739280672ef4a8d Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 25 Nov 2025 14:27:09 -0500 Subject: [PATCH 3/9] Fixed lint errors --- .../examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx index 2adec2287..4c1a0e1c2 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx @@ -59,7 +59,10 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => { 'alphabetical-desc': 'Name (Z-A)' }; - const onSortSelect = (_event: React.MouseEvent | undefined, value: string | number | undefined) => { + const onSortSelect = ( + _event: React.MouseEvent | undefined, + value: string | number | undefined + ) => { setSelectedSort(value as string); setIsSortSelectOpen(false); }; @@ -156,7 +159,8 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => { icon={ } From 62448e22424866ed5fc4ad5da677d0a8c2e4010f Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 25 Nov 2025 16:00:57 -0500 Subject: [PATCH 4/9] Added prop for custom search toolbar --- .../ChatbotHeaderDrawerWithSearchActions.tsx | 9 ++++++ .../ChatbotConversationHistoryNav.test.tsx | 29 +++++++++++++++++++ .../ChatbotConversationHistoryNav.tsx | 12 ++++++-- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx index 4c1a0e1c2..1462425ee 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx @@ -45,6 +45,7 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => { const [hasDrawerHeadDivider, setHasDrawerHeadDivider] = useState(false); const [showSearchActionStart, setShowSearchActionStart] = useState(false); const [showSearchActionEnd, setShowSearchActionEnd] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [isSortSelectOpen, setIsSortSelectOpen] = useState(false); const [selectedSort, setSelectedSort] = useState('newest'); const [conversations, setConversations] = useState( @@ -109,6 +110,13 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => { id="show-search-action-end" name="show-search-action-end" /> + setIsLoading(!isLoading)} + id="drawer-is-loading" + name="drawer-is-loading" + /> setIsDrawerOpen(!isDrawerOpen)} @@ -130,6 +138,7 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => { }} drawerContent={
Drawer content
} hasDrawerHeadDivider={hasDrawerHeadDivider} + isLoading={isLoading} searchActionStart={ showSearchActionStart ? (