From a8e97df3bdb91ec817cdf589f4a5cde159e4e160 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 15 Jul 2025 10:52:52 -0400 Subject: [PATCH 1/5] feat(Messages): added edit action --- .../chatbot/examples/Messages/Messages.md | 3 +- .../chatbot/examples/Messages/UserMessage.tsx | 32 ++++++++++++++++--- packages/module/src/Message/Message.test.tsx | 5 +++ packages/module/src/Message/Message.tsx | 8 +++-- packages/module/src/Message/MessageInput.tsx | 6 +++- .../ResponseActions/ResponseActions.test.tsx | 7 +++- .../src/ResponseActions/ResponseActions.tsx | 27 ++++++++++++++-- 7 files changed, 75 insertions(+), 13 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index 469c5d96f..70e8e5326 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -48,7 +48,7 @@ import { explorePipelinesQuickStart } from './explore-pipeline-quickstart.ts'; import { monitorSampleAppQuickStart } from '@patternfly/chatbot/src/Message/QuickStarts/monitor-sampleapp-quickstart.ts'; import userAvatar from './user_avatar.svg'; import squareImg from './PF-social-color-square.svg'; -import { CSSProperties, useState, Fragment, FunctionComponent, MouseEvent as ReactMouseEvent, KeyboardEvent as ReactKeyboardEvent, Ref, isValidElement, cloneElement, Children, ReactNode } from 'react'; +import { CSSProperties, useState, Fragment, FunctionComponent, MouseEvent as ReactMouseEvent, KeyboardEvent as ReactKeyboardEvent, Ref, isValidElement, cloneElement, Children, ReactNode, useRef, useEffect } from 'react'; The `content` prop of the `` component is passed to a `` component (from [react-markdown](https://remarkjs.github.io/react-markdown/)), which is configured to translate plain text strings into PatternFly [`` components](/components/content) and code blocks into PatternFly [`` components.](/components/code-block) @@ -72,6 +72,7 @@ You can add actions to a message, to allow users to interact with the message co - Feedback responses that allow users to rate a message as "good" or "bad". - Copy and share controls that allow users to share the message content with others. +- An edit action to allow users to edit a message they previously sent. This should only be applied to user messages - see the [user messages example](#user-messages) for details on how to implement this action. - A listen action, that will read the message content out loud. **Note:** The logic for the actions is not built into the component and must be implemented by the consuming application. diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx index dec595d67..72e284e9f 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx @@ -1,4 +1,4 @@ -import { Fragment, useState, CSSProperties, FunctionComponent, MouseEvent } from 'react'; +import { Fragment, useState, useRef, useEffect, CSSProperties, FunctionComponent, MouseEvent } from 'react'; import Message from '@patternfly/chatbot/dist/dynamic/Message'; import userAvatar from './user_avatar.svg'; import { @@ -12,12 +12,23 @@ import { import { rehypeCodeBlockToggle } from '@patternfly/chatbot/dist/esm/Message/Plugins/rehypeCodeBlockToggle'; export const UserMessageExample: FunctionComponent = () => { + const messageInputRef = useRef(null); const [variant, setVariant] = useState('Code'); - const [isEditable, setIsEditable] = useState(true); + const [isEditable, setIsEditable] = useState(false); + const [isSelectedEditable, setIsSelectedEditable] = useState(true); const [isOpen, setIsOpen] = useState(false); const [selected, setSelected] = useState('Message content type'); const [isExpandable, setIsExpanded] = useState(false); + useEffect(() => { + if (isEditable && messageInputRef?.current) { + messageInputRef.current.focus(); + const messageLength = messageInputRef.current.value.length; + // Mimic the behavior of the textarea when the user clicks on a label to place the cursor at the end of the input value + messageInputRef.current.setSelectionRange(messageLength, messageLength); + } + }, [isEditable]); + /* eslint-disable indent */ const renderContent = () => { switch (variant) { @@ -212,6 +223,17 @@ _Italic text, formatted with single underscores_ avatar={userAvatar} avatarProps={{ isBordered: true }} /> + setIsEditable(false)} + onEditCancel={() => setIsEditable(false)} + actions={{ edit: { onClick: () => setIsEditable(true) } }} + content="This is a user message with an edit action." + avatar={userAvatar} + inputRef={messageInputRef} + /> setIsSelectedEditable(false)} - onEditCancel={() => setIsSelectedEditable(false)} codeBlockProps={{ isExpandable, expandableSectionProps: { truncateMaxLines: isExpandable ? 1 : undefined } }} // In this example, custom plugin will override any custom expandedText or collapsedText attributes provided // The purpose of this plugin is to provide unique link names for the code blocks diff --git a/packages/module/src/ResponseActions/ResponseActions.tsx b/packages/module/src/ResponseActions/ResponseActions.tsx index 7284dfee9..5e816668f 100644 --- a/packages/module/src/ResponseActions/ResponseActions.tsx +++ b/packages/module/src/ResponseActions/ResponseActions.tsx @@ -7,7 +7,7 @@ import { OutlinedThumbsDownIcon, OutlinedCopyIcon, DownloadIcon, - EditIcon + PencilAltIcon } from '@patternfly/react-icons'; import ResponseActionButton from './ResponseActionButton'; import { ButtonProps, TooltipProps } from '@patternfly/react-core'; @@ -178,7 +178,7 @@ export const ResponseActions: FunctionComponent = ({ action tooltipContent={edit.tooltipContent ?? 'Edit '} clickedTooltipContent={edit.clickedTooltipContent ?? 'Editing'} tooltipProps={edit.tooltipProps} - icon={} + icon={} isClicked={activeButton === 'edit'} ref={edit.ref} aria-expanded={edit['aria-expanded']} From 4ae9edb4f1a67766a0d83e4030bcaf5d5e5c00a7 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Wed, 16 Jul 2025 11:26:49 -0400 Subject: [PATCH 3/5] Updated examples and prop desc verbiage --- .../content/extensions/chatbot/examples/Messages/Messages.md | 2 +- packages/module/src/Message/MessageInput.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index d344841ff..cf4eaeecb 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -184,7 +184,7 @@ The quick start tile displayed below the message is based on the tile included i Messages from users have a different background color to differentiate them from bot messages. You can also display a custom avatar that is uploaded by the user. You can further customize the avatar by applying an additional class or passing [PatternFly avatar props](/components/avatar) to the `` component via `avatarProps`. -User messages can also be made editable by passing an "edit" object to the `actions` property. Note that when editing is enabled focus should be placed on the text area, and when editing is completed or canceled the focus should be moved back to the edit button. +User messages can also be made editable by passing an "edit" object to the `actions` property. When editing is enabled focus should be placed on the text area. When editing is completed or canceled the focus should be moved back to the edit button. ```js file="./UserMessage.tsx" diff --git a/packages/module/src/Message/MessageInput.tsx b/packages/module/src/Message/MessageInput.tsx index df3f91733..02f42c6b0 100644 --- a/packages/module/src/Message/MessageInput.tsx +++ b/packages/module/src/Message/MessageInput.tsx @@ -16,7 +16,7 @@ export interface MessageInputProps extends FormProps { onEditUpdate?: (event: React.MouseEvent, value: string) => void; /** Callback functionf or when edit cancel update button is clicked */ onEditCancel?: (event: React.MouseEvent) => void; - /** Ref applied to editbale message input */ + /** Ref applied to editable message input */ inputRef?: Ref; /** Message text */ content?: string; From 5af589d2dbaf3806e7de3b0b6dad5d38de1da6d4 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Wed, 16 Jul 2025 11:48:37 -0400 Subject: [PATCH 4/5] Updated last bale --- packages/module/src/Message/Message.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index 784a3382f..64ee56827 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -179,7 +179,7 @@ export interface MessageProps extends Omit, 'role'> { onEditUpdate?: (event: ReactMouseEvent) => void; /** Callback functionf or when edit cancel update button is clicked */ onEditCancel?: (event: ReactMouseEvent) => void; - /** Ref applied to editbale message input */ + /** Ref applied to editable message input */ inputRef?: Ref; /** Props for edit form */ editFormProps?: FormProps; From 953a91969bb6ef92effa83527197198af7506201 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 17 Jul 2025 13:56:00 -0400 Subject: [PATCH 5/5] Removed actions when isEditable is true --- .../chatbot/examples/Messages/UserMessage.tsx | 15 +++++++--- packages/module/src/Message/Message.test.tsx | 30 +++++++++++++++++++ packages/module/src/Message/Message.tsx | 2 +- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx index 7465164ba..482a207ff 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx @@ -15,11 +15,13 @@ export const UserMessageExample: FunctionComponent = () => { const messageInputRef = useRef(null); const editButtonRef = useRef(null); const [variant, setVariant] = useState('Code'); - const [isEditable, setIsEditable] = useState(false); const [isOpen, setIsOpen] = useState(false); const [selected, setSelected] = useState('Message content type'); const [isExpandable, setIsExpanded] = useState(false); + const [isEditable, setIsEditable] = useState(false); + const prevIsEditable = useRef(false); + useEffect(() => { if (isEditable && messageInputRef?.current) { messageInputRef.current.focus(); @@ -27,6 +29,13 @@ export const UserMessageExample: FunctionComponent = () => { // Mimic the behavior of the textarea when the user clicks on a label to place the cursor at the end of the input value messageInputRef.current.setSelectionRange(messageLength, messageLength); } + + // We only want to re-focus the edit action button if the user has previously clicked on it, + // and prevent it from receiving focus on page load + if (prevIsEditable.current && !isEditable && editButtonRef?.current) { + editButtonRef.current.focus(); + prevIsEditable.current = false; + } }, [isEditable]); /* eslint-disable indent */ @@ -192,10 +201,8 @@ _Italic text, formatted with single underscores_ }; const onUpdateOrCancelEdit = () => { + prevIsEditable.current = isEditable; setIsEditable(false); - if (editButtonRef?.current) { - editButtonRef.current.focus(); - } }; const toggle = (toggleRef: Ref) => ( diff --git a/packages/module/src/Message/Message.test.tsx b/packages/module/src/Message/Message.test.tsx index 81fa5351e..81363e5c8 100644 --- a/packages/module/src/Message/Message.test.tsx +++ b/packages/module/src/Message/Message.test.tsx @@ -472,6 +472,36 @@ describe('Message', () => { expect(screen.queryByRole('button', { name: label })).toBeFalsy(); }); }); + it('should not show actions if isEditable is true', async () => { + render( + console.log('Good response') }, + // eslint-disable-next-line no-console + negative: { onClick: () => console.log('Bad response') }, + // eslint-disable-next-line no-console + copy: { onClick: () => console.log('Copy') }, + // eslint-disable-next-line no-console + edit: { onClick: () => console.log('Edit') }, + // eslint-disable-next-line no-console + share: { onClick: () => console.log('Share') }, + // eslint-disable-next-line no-console + download: { onClick: () => console.log('Download') }, + // eslint-disable-next-line no-console + listen: { onClick: () => console.log('Listen') } + }} + /> + ); + ALL_ACTIONS.forEach(({ label }) => { + expect(screen.queryByRole('button', { name: label })).toBeFalsy(); + }); + }); it('should render unordered lists correctly', () => { render(); expect(screen.getByText('Here is an unordered list:')).toBeTruthy(); diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index 64ee56827..4032eb6db 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -373,7 +373,7 @@ export const MessageBase: FunctionComponent = ({ isCompact={isCompact} /> )} - {!isLoading && actions && } + {!isLoading && !isEditable && actions && } {userFeedbackForm && } {userFeedbackComplete && (