From 5ec1ca72a9eeebe7715993be5f981159235cf918 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Thu, 28 Aug 2025 17:03:17 -0400 Subject: [PATCH 1/3] feat(FilePreview): Add file preview modal Assisted-by: Cursor (test boilerplate) --- .../chatbot/examples/Messages/FilePreview.tsx | 33 ++++++ .../chatbot/examples/Messages/Messages.md | 9 ++ .../extensions/chatbot/examples/UI/UI.md | 1 + .../module/src/FilePreview/FilePreview.scss | 22 ++++ .../src/FilePreview/FilePreview.test.tsx | 112 ++++++++++++++++++ .../module/src/FilePreview/FilePreview.tsx | 58 +++++++++ packages/module/src/FilePreview/index.ts | 3 + packages/module/src/index.ts | 3 + packages/module/src/main.scss | 1 + 9 files changed, 242 insertions(+) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx create mode 100644 packages/module/src/FilePreview/FilePreview.scss create mode 100644 packages/module/src/FilePreview/FilePreview.test.tsx create mode 100644 packages/module/src/FilePreview/FilePreview.tsx create mode 100644 packages/module/src/FilePreview/index.ts diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx new file mode 100644 index 000000000..5f38ac5e5 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx @@ -0,0 +1,33 @@ +import { useState, FunctionComponent, MouseEvent as ReactMouseEvent } from 'react'; +import { Button, Checkbox } from '@patternfly/react-core'; +import FilePreview from '@patternfly/chatbot/dist/dynamic/FilePreview'; + +export const AttachmentEditModalExample: FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [isCompact, setIsCompact] = useState(false); + + const handleModalToggle = (_event: ReactMouseEvent | MouseEvent | KeyboardEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + <> + setIsCompact(!isCompact)} + id="modal-compact-no-preview" + name="modal-compact-no-preview" + > + + + Preview is not supported for this file type + + + ); +}; 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 8fff11d4b..4c77fe7be 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 @@ -47,6 +47,7 @@ import { monitorSampleAppQuickStart } from '@patternfly/chatbot/src/Message/Quic 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, useRef, useEffect } from 'react'; +import FilePreview from '@patternfly/chatbot/dist/dynamic/FilePreview'; 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) @@ -255,6 +256,14 @@ To allow users to edit an attached file, load a new code editor within the ChatB ``` +### File preview + +If the contents of an attachment cannot be previewed, load a view of the file name and other information in a new modal. Return users to the main ChatBot window once they dismiss the modal. + +```js file="./FilePreview.tsx" + +``` + ### Failed attachment error When an attachment upload fails, a [danger alert](/components/alert) is displayed to provide details about the reason for failure. 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 1afae00ed..26fb43d15 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 @@ -87,6 +87,7 @@ 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, useEffect} from 'react'; +import FilePreview from '@patternfly/chatbot/dist/dynamic/FilePreview'; ## Structure diff --git a/packages/module/src/FilePreview/FilePreview.scss b/packages/module/src/FilePreview/FilePreview.scss new file mode 100644 index 000000000..90b9f80a3 --- /dev/null +++ b/packages/module/src/FilePreview/FilePreview.scss @@ -0,0 +1,22 @@ +.pf-chatbot__file-preview-body { + display: flex; + flex-direction: column; + gap: var(--pf-t--global--spacer--md); + align-items: center; + justify-content: center; +} + +.pf-chatbot__file-preview-icon { + color: var(--pf-t--global--icon--color--subtle); + width: var(--pf-t--global--icon--size--2xl); + height: var(--pf-t--global--icon--size--2xl); +} + +.pf-chatbot__file-preview-name { + font-size: var(--pf-t--global--font--size--xl); + font-weight: var(--pf-t--global--font--weight--heading--default); +} +.pf-chatbot__file-preview-body { + color: var(--pf-t--global--text--color--subtle); + font-size: var(--pf-t--global--font--size--body--lg); +} diff --git a/packages/module/src/FilePreview/FilePreview.test.tsx b/packages/module/src/FilePreview/FilePreview.test.tsx new file mode 100644 index 000000000..8b78edec9 --- /dev/null +++ b/packages/module/src/FilePreview/FilePreview.test.tsx @@ -0,0 +1,112 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import FilePreview from './FilePreview'; +import { ChatbotDisplayMode } from '../Chatbot'; +import { Button, ModalBodyProps, ModalHeaderProps } from '@patternfly/react-core'; + +describe('FilePreview', () => { + const defaultProps = { + isModalOpen: true, + handleModalToggle: jest.fn(), + fileName: 'test-file.txt', + children: 'File content preview' + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render with basic props', () => { + render(); + expect(screen.getByText('Preview file')).toBeInTheDocument(); + expect(screen.getByText('test-file.txt')).toBeInTheDocument(); + }); + + it('should render with custom title', () => { + const customTitle = 'Custom file preview title'; + render(); + expect(screen.getByRole('heading', { name: customTitle })).toBeTruthy(); + }); + + it('should handle modal toggle when closed', () => { + const mockToggle = jest.fn(); + render(); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + it('should apply default display mode class', () => { + render(); + const modal = screen.getByRole('dialog'); + expect(modal).toHaveClass('pf-chatbot__file-preview-modal--default'); + }); + + it('should apply custom display mode class', () => { + render(); + const modal = screen.getByRole('dialog'); + expect(modal).toHaveClass('pf-chatbot__file-preview-modal--fullscreen'); + }); + + it('should apply compact styling when isCompact is true', () => { + render(); + const modal = screen.getByRole('dialog'); + expect(modal).toHaveClass('pf-m-compact'); + }); + + it('should not apply compact styling when isCompact is false', () => { + render(); + const modal = screen.getByRole('dialog'); + expect(modal).not.toHaveClass('pf-m-compact'); + }); + + it('should apply custom className', () => { + const customClass = 'custom-file-preview'; + render(); + const modal = screen.getByRole('dialog'); + expect(modal).toHaveClass(customClass); + }); + + it('should pass through additional props to ChatbotModal', () => { + render(); + const modal = screen.getByTestId('file-preview-modal'); + expect(modal).toBeInTheDocument(); + }); + + it('should pass modalHeaderProps to ModalHeader', () => { + const modalHeaderProps = { + 'data-testid': 'custom-header' + } as ModalHeaderProps; + render(); + const header = screen.getByTestId('custom-header'); + expect(header).toBeInTheDocument(); + }); + + it('should pass modalBodyProps to ModalBody', () => { + const modalBodyProps = { + 'data-testid': 'custom-body' + } as ModalBodyProps; + render(); + const body = screen.getByTestId('custom-body'); + expect(body).toBeInTheDocument(); + }); + + it('should pass ouiaId to ChatbotModal', () => { + const ouiaId = 'file-preview-ouia-id'; + render(); + const modal = screen.getByRole('dialog'); + expect(modal).toHaveAttribute('data-ouia-component-id', ouiaId); + }); + + it('should handle complex children', () => { + const complexChildren = ( +
+

File details

+

Size: 1.2 MB

+ +
+ ); + render({complexChildren}); + expect(screen.getByRole('heading', { name: /File details/i })).toBeTruthy(); + expect(screen.getByText('Size: 1.2 MB')).toBeTruthy(); + expect(screen.getByRole('button', { name: /Download/i })).toBeTruthy(); + }); +}); diff --git a/packages/module/src/FilePreview/FilePreview.tsx b/packages/module/src/FilePreview/FilePreview.tsx new file mode 100644 index 000000000..455d1c285 --- /dev/null +++ b/packages/module/src/FilePreview/FilePreview.tsx @@ -0,0 +1,58 @@ +import { ModalBody, ModalBodyProps, ModalHeader, ModalHeaderProps } from '@patternfly/react-core'; +import type { FunctionComponent } from 'react'; +import { ChatbotDisplayMode } from '../Chatbot'; +import ChatbotModal, { ChatbotModalProps } from '../ChatbotModal'; +import { FileIcon } from '@patternfly/react-icons'; + +export interface FilePreviewProps extends ChatbotModalProps { + /** Class applied to modal */ + className?: string; + /** Function that handles modal toggle */ + handleModalToggle: (event: React.MouseEvent | MouseEvent | KeyboardEvent) => void; + /** Whether modal is open */ + isModalOpen: boolean; + /** Title of modal */ + title?: string; + /** Display mode for the Chatbot parent; this influences the styles applied */ + displayMode?: ChatbotDisplayMode; + /** File name */ + fileName: string; + /** Sets modal to compact styling. */ + isCompact?: boolean; + /** Additional props passed to modal header */ + modalHeaderProps?: ModalHeaderProps; + /** Additional props passed to modal body */ + modalBodyProps?: ModalBodyProps; +} + +const FilePreview: FunctionComponent = ({ + isModalOpen, + displayMode = ChatbotDisplayMode.default, + children, + fileName, + isCompact, + className, + handleModalToggle, + title = 'Preview file', + modalHeaderProps, + modalBodyProps, + ...props +}: FilePreviewProps) => ( + + + + +

{fileName}

+ {children &&
{children}
} +
+
+); + +export default FilePreview; diff --git a/packages/module/src/FilePreview/index.ts b/packages/module/src/FilePreview/index.ts new file mode 100644 index 000000000..9d51281e8 --- /dev/null +++ b/packages/module/src/FilePreview/index.ts @@ -0,0 +1,3 @@ +export { default } from './FilePreview'; + +export * from './FilePreview'; diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 98167b41d..84eacae53 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -51,6 +51,9 @@ export * from './FileDetailsLabel'; export { default as FileDropZone } from './FileDropZone'; export * from './FileDropZone'; +export { default as FilePreview } from './FilePreview'; +export * from './FilePreview'; + export { default as LoadingMessage } from './LoadingMessage'; export * from './LoadingMessage'; diff --git a/packages/module/src/main.scss b/packages/module/src/main.scss index 699a59bd1..3f6a43ab4 100644 --- a/packages/module/src/main.scss +++ b/packages/module/src/main.scss @@ -14,6 +14,7 @@ @import './FileDetails/FileDetails'; @import './FileDetailsLabel/FileDetailsLabel'; @import './FileDropZone/FileDropZone'; +@import './FilePreview/FilePreview.scss'; @import './Message/Message'; @import './Message/CodeBlockMessage/CodeBlockMessage'; @import './Message/ImageMessage/ImageMessage'; From 86a7b43e0ccade9f10925ce18646c3d2e6f9ec06 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Tue, 2 Sep 2025 10:22:30 -0400 Subject: [PATCH 2/3] Text edits Co-authored-by: Erin Donehoo <105813956+edonehoo@users.noreply.github.com> --- .../extensions/chatbot/examples/Messages/FilePreview.tsx | 2 +- .../content/extensions/chatbot/examples/Messages/Messages.md | 2 +- packages/module/src/FilePreview/FilePreview.test.tsx | 2 +- packages/module/src/FilePreview/FilePreview.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx index 5f38ac5e5..b111c595b 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx @@ -26,7 +26,7 @@ export const AttachmentEditModalExample: FunctionComponent = () => { fileName="compressed-file.zip" isCompact={isCompact} > - Preview is not supported for this file type + Preview unavailable
); 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 4c77fe7be..a18895bf8 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 @@ -258,7 +258,7 @@ To allow users to edit an attached file, load a new code editor within the ChatB ### File preview -If the contents of an attachment cannot be previewed, load a view of the file name and other information in a new modal. Return users to the main ChatBot window once they dismiss the modal. +If the contents of an attachment cannot be previewed, load a file preview modal with a view of the file name and an unavailable message. When users close the modal, return to the main ChatBot window. ```js file="./FilePreview.tsx" diff --git a/packages/module/src/FilePreview/FilePreview.test.tsx b/packages/module/src/FilePreview/FilePreview.test.tsx index 8b78edec9..e819d04df 100644 --- a/packages/module/src/FilePreview/FilePreview.test.tsx +++ b/packages/module/src/FilePreview/FilePreview.test.tsx @@ -18,7 +18,7 @@ describe('FilePreview', () => { it('should render with basic props', () => { render(); - expect(screen.getByText('Preview file')).toBeInTheDocument(); + expect(screen.getByText('File preview')).toBeInTheDocument(); expect(screen.getByText('test-file.txt')).toBeInTheDocument(); }); diff --git a/packages/module/src/FilePreview/FilePreview.tsx b/packages/module/src/FilePreview/FilePreview.tsx index 455d1c285..adbc53ffa 100644 --- a/packages/module/src/FilePreview/FilePreview.tsx +++ b/packages/module/src/FilePreview/FilePreview.tsx @@ -33,7 +33,7 @@ const FilePreview: FunctionComponent = ({ isCompact, className, handleModalToggle, - title = 'Preview file', + title = 'File preview', modalHeaderProps, modalBodyProps, ...props From 9b087cbb2e1a403e3c0134710c07d4fd50043ee9 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Fri, 5 Sep 2025 10:33:17 -0400 Subject: [PATCH 3/3] Address Eric feedback --- .../extensions/chatbot/examples/Messages/FilePreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx index b111c595b..a3cc14960 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx @@ -19,7 +19,7 @@ export const AttachmentEditModalExample: FunctionComponent = () => { id="modal-compact-no-preview" name="modal-compact-no-preview" > - +