Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// From Cursor, with aid
import React, { FunctionComponent, useState, useRef, useEffect } from 'react';
import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
import ChatbotConversationHistoryNav, {
Conversation
} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
import { ChatbotModal } from '@patternfly/chatbot/dist/dynamic/ChatbotModal';
import {
Checkbox,
DropdownItem,
DropdownList,
Button,
TextInput,
Form,
FormGroup,
ModalHeader,
ModalBody,
ModalFooter
} from '@patternfly/react-core';

export const ChatbotHeaderTitleDemo: FunctionComponent = () => {
const [isDrawerOpen, setIsDrawerOpen] = useState(true);
const displayMode = ChatbotDisplayMode.embedded;

// Modal state
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingConversationId, setEditingConversationId] = useState<string | number | null>(null);
const [editingText, setEditingText] = useState('');

// Ref for the text input
const textInputRef = useRef<HTMLInputElement>(null);

// Focus the text input when modal opens
useEffect(() => {
if (isModalOpen && textInputRef.current) {
textInputRef.current.focus();
// Move cursor to the end of the text
const length = textInputRef.current.value.length;
textInputRef.current.setSelectionRange(length, length);
}
}, [isModalOpen]);

const findConversationAndGroup = (conversations: { [key: string]: Conversation[] }, itemId: string | number) => {
for (const [groupKey, conversationList] of Object.entries(conversations)) {
const conversationIndex = conversationList.findIndex((conv) => conv.id === itemId);
if (conversationIndex !== -1) {
return { groupKey, conversationIndex, conversation: conversationList[conversationIndex] };
}
}
return null;
};

const onRenameClick = (itemId: string | number) => {
const result = findConversationAndGroup(conversations, itemId);
if (result) {
setEditingConversationId(itemId);
setEditingText(result.conversation.text);
setIsModalOpen(true);
}
};

const handleModalSave = () => {
if (editingConversationId) {
setConversations((prevConversations) => {
const result = findConversationAndGroup(prevConversations, editingConversationId);
if (!result) {
return prevConversations;
}

const { groupKey, conversationIndex } = result;
const newConversations = { ...prevConversations };
const newGroup = [...newConversations[groupKey]];

newGroup[conversationIndex] = { ...newGroup[conversationIndex], text: editingText };
newConversations[groupKey] = newGroup;

return newConversations;
});
}
handleModalClose();
};

const handleModalCancel = () => {
handleModalClose();
};

const handleModalClose = () => {
setIsModalOpen(false);
setEditingConversationId(null);
setEditingText('');
};

const handleTextInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
handleModalSave();
}
};

const renderMenuItems = (itemId: string | number) => [
<DropdownList key={`list-${itemId}`}>
<DropdownItem value="Download" id="Download">
Download
</DropdownItem>
<DropdownItem value="Rename" id="Rename" onClick={() => onRenameClick(itemId)}>
Rename
</DropdownItem>
<DropdownItem value="Archive" id="Archive">
Archive
</DropdownItem>
<DropdownItem value="Delete" id="Delete">
Delete
</DropdownItem>
</DropdownList>
];

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' }
]
};

const [conversations, setConversations] = useState(initialConversations);

// Create conversations with menu items dynamically
const conversationsWithMenuItems = () => {
const newConversations = { ...conversations };
Object.keys(newConversations).forEach((groupKey) => {
newConversations[groupKey] = newConversations[groupKey].map((conv) => ({
...conv,
menuItems: renderMenuItems(conv.id)
}));
});
return newConversations;
};

return (
<>
<Checkbox
label="Display drawer"
isChecked={isDrawerOpen}
onChange={() => setIsDrawerOpen(!isDrawerOpen)}
id="drawer-actions-visible"
name="drawer-actions-visible"
></Checkbox>
<ChatbotConversationHistoryNav
displayMode={displayMode}
onDrawerToggle={() => setIsDrawerOpen(!isDrawerOpen)}
isDrawerOpen={isDrawerOpen}
setIsDrawerOpen={setIsDrawerOpen}
conversations={conversationsWithMenuItems()}
drawerContent={<div>Drawer content</div>}
/>

<ChatbotModal displayMode={displayMode} isOpen={isModalOpen} onClose={handleModalClose}>
<ModalHeader title="Rename Conversation" />
<ModalBody>
<Form>
<FormGroup label="Conversation Name" fieldId="conversation-name" isRequired>
<TextInput
isRequired
ref={textInputRef}
value={editingText}
onChange={(_, value) => setEditingText(value)}
onKeyDown={handleTextInputKeyDown}
id="conversation-name"
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button key="save" variant="primary" onClick={handleModalSave}>
Save
</Button>
<Button key="cancel" variant="link" onClick={handleModalCancel}>
Cancel
</Button>
</ModalFooter>
</ChatbotModal>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ import userAvatar from '../Messages/user_avatar.svg';
import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
import termsAndConditionsHeader from './PF-TermsAndConditionsHeader.svg';
import { CloseIcon, SearchIcon, OutlinedCommentsIcon } from '@patternfly/react-icons';
import { FunctionComponent, FormEvent, useState, useRef, MouseEvent, isValidElement, cloneElement, Children, ReactNode, Ref, MouseEvent as ReactMouseEvent, CSSProperties} from 'react';
import { FunctionComponent, FormEvent, useState, useRef, MouseEvent, isValidElement, cloneElement, Children, ReactNode, Ref, MouseEvent as ReactMouseEvent, CSSProperties, useEffect} from 'react';

## Structure

Expand Down Expand Up @@ -374,6 +374,19 @@ To help users track important conversations, add a "pin" option to the conversat

```

### Renaming conversations in history drawer

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

- When the modal opens, focus is placed at the end of the text input.
- When the modal closes, focus goes back to the action toggle that was previously opened.
- Changes can be canceled via the **<kbd>Escape</kbd>** key or clicking a "Cancel" button.
- Changes can be saved via the **<kbd>Enter</kbd>** key or by clicking a "Save" button.

```js file="./ChatbotConversationEditing.tsx"

```

### Drawer with active conversation

If you're showing a conversation that is already active, you can set the `activeItemId` prop on your `<ChatbotConversationHistoryNav>` to apply an active visual state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@

it('should render the dropdown', () => {
render(<ChatbotConversationHistoryDropdown menuItems={menuItems} menuClassName="custom-class" />);
expect(screen.queryByRole('menuitem', { name: /Conversation options/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Conversation options/i })).toBeInTheDocument();
});

it('should display the dropdown menuItems', () => {
render(<ChatbotConversationHistoryDropdown menuItems={menuItems} />);

const toggle = screen.queryByRole('menuitem', { name: /Conversation options/i })!;
const toggle = screen.queryByRole('button', { name: /Conversation options/i })!;

Check warning on line 23 in packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx

View workflow job for this annotation

GitHub Actions / call-build-lint-test-workflow / lint

Forbidden non-null assertion

expect(toggle).toBeInTheDocument();
fireEvent.click(toggle);
Expand All @@ -33,7 +33,7 @@

it('should invoke onSelect callback when menuitem is clicked', () => {
render(<ChatbotConversationHistoryDropdown menuItems={menuItems} onSelect={onSelect} />);
const toggle = screen.queryByRole('menuitem', { name: /Conversation options/i })!;
const toggle = screen.queryByRole('button', { name: /Conversation options/i })!;

Check warning on line 36 in packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx

View workflow job for this annotation

GitHub Actions / call-build-lint-test-workflow / lint

Forbidden non-null assertion
fireEvent.click(toggle);
fireEvent.click(screen.getByText('Rename'));

Expand All @@ -42,7 +42,7 @@

it('should toggle the dropdown when menuitem is clicked', () => {
render(<ChatbotConversationHistoryDropdown menuItems={menuItems} onSelect={onSelect} />);
const toggle = screen.queryByRole('menuitem', { name: /Conversation options/i })!;
const toggle = screen.queryByRole('button', { name: /Conversation options/i })!;

Check warning on line 45 in packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx

View workflow job for this annotation

GitHub Actions / call-build-lint-test-workflow / lint

Forbidden non-null assertion
fireEvent.click(toggle);
fireEvent.click(screen.getByText('Delete'));

Expand All @@ -53,18 +53,18 @@

it('should close the dropdown when user clicks outside', () => {
render(<ChatbotConversationHistoryDropdown menuItems={menuItems} onSelect={onSelect} />);
const toggle = screen.queryByRole('menuitem', { name: /Conversation options/i })!;
const toggle = screen.queryByRole('button', { name: /Conversation options/i })!;

Check warning on line 56 in packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx

View workflow job for this annotation

GitHub Actions / call-build-lint-test-workflow / lint

Forbidden non-null assertion
fireEvent.click(toggle);

expect(screen.queryByText('Delete')).toBeInTheDocument();
fireEvent.click(toggle.parentElement!);

Check warning on line 60 in packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx

View workflow job for this annotation

GitHub Actions / call-build-lint-test-workflow / lint

Forbidden non-null assertion

expect(screen.queryByText('Delete')).not.toBeInTheDocument();
});

it('should show the tooltip when the user hovers over the toggle button', async () => {
render(<ChatbotConversationHistoryDropdown menuItems={menuItems} label="Actions dropdown" />);
const toggle = screen.queryByRole('menuitem', { name: /Actions dropdown/i })!;
const toggle = screen.queryByRole('button', { name: /Actions dropdown/i })!;

Check warning on line 67 in packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx

View workflow job for this annotation

GitHub Actions / call-build-lint-test-workflow / lint

Forbidden non-null assertion

fireEvent(
toggle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export const ChatbotConversationHistoryDropdown: FunctionComponent<ChatbotConver
ref={toggleRef}
isExpanded={isOpen}
onClick={() => setIsOpen(!isOpen)}
role="menuitem"
>
<EllipsisIcon />
</MenuToggle>
Expand Down
Loading
Loading