Skip to content

Commit 399d013

Browse files
authored
Merge pull request #664 from rebeccaalpert/preview
feat(FilePreview): Add file preview modal
2 parents b296cc3 + 9b087cb commit 399d013

File tree

9 files changed

+242
-0
lines changed

9 files changed

+242
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { useState, FunctionComponent, MouseEvent as ReactMouseEvent } from 'react';
2+
import { Button, Checkbox } from '@patternfly/react-core';
3+
import FilePreview from '@patternfly/chatbot/dist/dynamic/FilePreview';
4+
5+
export const AttachmentEditModalExample: FunctionComponent = () => {
6+
const [isModalOpen, setIsModalOpen] = useState(false);
7+
const [isCompact, setIsCompact] = useState(false);
8+
9+
const handleModalToggle = (_event: ReactMouseEvent | MouseEvent | KeyboardEvent) => {
10+
setIsModalOpen(!isModalOpen);
11+
};
12+
13+
return (
14+
<>
15+
<Checkbox
16+
label="Show compact version"
17+
isChecked={isCompact}
18+
onChange={() => setIsCompact(!isCompact)}
19+
id="modal-compact-no-preview"
20+
name="modal-compact-no-preview"
21+
></Checkbox>
22+
<Button onClick={handleModalToggle}>Launch file preview modal</Button>
23+
<FilePreview
24+
isModalOpen={isModalOpen}
25+
handleModalToggle={handleModalToggle}
26+
fileName="compressed-file.zip"
27+
isCompact={isCompact}
28+
>
29+
Preview unavailable
30+
</FilePreview>
31+
</>
32+
);
33+
};

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { monitorSampleAppQuickStart } from '@patternfly/chatbot/src/Message/Quic
4747
import userAvatar from './user_avatar.svg';
4848
import squareImg from './PF-social-color-square.svg';
4949
import { CSSProperties, useState, Fragment, FunctionComponent, MouseEvent as ReactMouseEvent, KeyboardEvent as ReactKeyboardEvent, Ref, isValidElement, cloneElement, Children, ReactNode, useRef, useEffect } from 'react';
50+
import FilePreview from '@patternfly/chatbot/dist/dynamic/FilePreview';
5051

5152
The `content` prop of the `<Message>` component is passed to a `<Markdown>` component (from [react-markdown](https://remarkjs.github.io/react-markdown/)), which is configured to translate plain text strings into PatternFly [`<Content>` components](/components/content) and code blocks into PatternFly [`<CodeBlock>` components.](/components/code-block)
5253

@@ -273,6 +274,14 @@ To allow users to edit an attached file, load a new code editor within the ChatB
273274

274275
```
275276

277+
### File preview
278+
279+
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.
280+
281+
```js file="./FilePreview.tsx"
282+
283+
```
284+
276285
### Failed attachment error
277286

278287
When an attachment upload fails, a [danger alert](/components/alert) is displayed to provide details about the reason for failure.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
8787
import termsAndConditionsHeader from './PF-TermsAndConditionsHeader.svg';
8888
import { CloseIcon, SearchIcon, OutlinedCommentsIcon } from '@patternfly/react-icons';
8989
import { FunctionComponent, FormEvent, useState, useRef, MouseEvent, isValidElement, cloneElement, Children, ReactNode, Ref, MouseEvent as ReactMouseEvent, CSSProperties, useEffect} from 'react';
90+
import FilePreview from '@patternfly/chatbot/dist/dynamic/FilePreview';
9091

9192
## Structure
9293

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.pf-chatbot__file-preview-body {
2+
display: flex;
3+
flex-direction: column;
4+
gap: var(--pf-t--global--spacer--md);
5+
align-items: center;
6+
justify-content: center;
7+
}
8+
9+
.pf-chatbot__file-preview-icon {
10+
color: var(--pf-t--global--icon--color--subtle);
11+
width: var(--pf-t--global--icon--size--2xl);
12+
height: var(--pf-t--global--icon--size--2xl);
13+
}
14+
15+
.pf-chatbot__file-preview-name {
16+
font-size: var(--pf-t--global--font--size--xl);
17+
font-weight: var(--pf-t--global--font--weight--heading--default);
18+
}
19+
.pf-chatbot__file-preview-body {
20+
color: var(--pf-t--global--text--color--subtle);
21+
font-size: var(--pf-t--global--font--size--body--lg);
22+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { render, screen } from '@testing-library/react';
2+
import '@testing-library/jest-dom';
3+
import FilePreview from './FilePreview';
4+
import { ChatbotDisplayMode } from '../Chatbot';
5+
import { Button, ModalBodyProps, ModalHeaderProps } from '@patternfly/react-core';
6+
7+
describe('FilePreview', () => {
8+
const defaultProps = {
9+
isModalOpen: true,
10+
handleModalToggle: jest.fn(),
11+
fileName: 'test-file.txt',
12+
children: 'File content preview'
13+
};
14+
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
});
18+
19+
it('should render with basic props', () => {
20+
render(<FilePreview {...defaultProps} />);
21+
expect(screen.getByText('File preview')).toBeInTheDocument();
22+
expect(screen.getByText('test-file.txt')).toBeInTheDocument();
23+
});
24+
25+
it('should render with custom title', () => {
26+
const customTitle = 'Custom file preview title';
27+
render(<FilePreview {...defaultProps} title={customTitle} />);
28+
expect(screen.getByRole('heading', { name: customTitle })).toBeTruthy();
29+
});
30+
31+
it('should handle modal toggle when closed', () => {
32+
const mockToggle = jest.fn();
33+
render(<FilePreview {...defaultProps} isModalOpen={false} handleModalToggle={mockToggle} />);
34+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
35+
});
36+
37+
it('should apply default display mode class', () => {
38+
render(<FilePreview {...defaultProps} />);
39+
const modal = screen.getByRole('dialog');
40+
expect(modal).toHaveClass('pf-chatbot__file-preview-modal--default');
41+
});
42+
43+
it('should apply custom display mode class', () => {
44+
render(<FilePreview {...defaultProps} displayMode={ChatbotDisplayMode.fullscreen} />);
45+
const modal = screen.getByRole('dialog');
46+
expect(modal).toHaveClass('pf-chatbot__file-preview-modal--fullscreen');
47+
});
48+
49+
it('should apply compact styling when isCompact is true', () => {
50+
render(<FilePreview {...defaultProps} isCompact />);
51+
const modal = screen.getByRole('dialog');
52+
expect(modal).toHaveClass('pf-m-compact');
53+
});
54+
55+
it('should not apply compact styling when isCompact is false', () => {
56+
render(<FilePreview {...defaultProps} isCompact={false} />);
57+
const modal = screen.getByRole('dialog');
58+
expect(modal).not.toHaveClass('pf-m-compact');
59+
});
60+
61+
it('should apply custom className', () => {
62+
const customClass = 'custom-file-preview';
63+
render(<FilePreview {...defaultProps} className={customClass} />);
64+
const modal = screen.getByRole('dialog');
65+
expect(modal).toHaveClass(customClass);
66+
});
67+
68+
it('should pass through additional props to ChatbotModal', () => {
69+
render(<FilePreview {...defaultProps} data-testid="file-preview-modal" />);
70+
const modal = screen.getByTestId('file-preview-modal');
71+
expect(modal).toBeInTheDocument();
72+
});
73+
74+
it('should pass modalHeaderProps to ModalHeader', () => {
75+
const modalHeaderProps = {
76+
'data-testid': 'custom-header'
77+
} as ModalHeaderProps;
78+
render(<FilePreview {...defaultProps} modalHeaderProps={modalHeaderProps} />);
79+
const header = screen.getByTestId('custom-header');
80+
expect(header).toBeInTheDocument();
81+
});
82+
83+
it('should pass modalBodyProps to ModalBody', () => {
84+
const modalBodyProps = {
85+
'data-testid': 'custom-body'
86+
} as ModalBodyProps;
87+
render(<FilePreview {...defaultProps} modalBodyProps={modalBodyProps} />);
88+
const body = screen.getByTestId('custom-body');
89+
expect(body).toBeInTheDocument();
90+
});
91+
92+
it('should pass ouiaId to ChatbotModal', () => {
93+
const ouiaId = 'file-preview-ouia-id';
94+
render(<FilePreview {...defaultProps} ouiaId={ouiaId} />);
95+
const modal = screen.getByRole('dialog');
96+
expect(modal).toHaveAttribute('data-ouia-component-id', ouiaId);
97+
});
98+
99+
it('should handle complex children', () => {
100+
const complexChildren = (
101+
<div>
102+
<h3>File details</h3>
103+
<p>Size: 1.2 MB</p>
104+
<Button>Download</Button>
105+
</div>
106+
);
107+
render(<FilePreview {...defaultProps}>{complexChildren}</FilePreview>);
108+
expect(screen.getByRole('heading', { name: /File details/i })).toBeTruthy();
109+
expect(screen.getByText('Size: 1.2 MB')).toBeTruthy();
110+
expect(screen.getByRole('button', { name: /Download/i })).toBeTruthy();
111+
});
112+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { ModalBody, ModalBodyProps, ModalHeader, ModalHeaderProps } from '@patternfly/react-core';
2+
import type { FunctionComponent } from 'react';
3+
import { ChatbotDisplayMode } from '../Chatbot';
4+
import ChatbotModal, { ChatbotModalProps } from '../ChatbotModal';
5+
import { FileIcon } from '@patternfly/react-icons';
6+
7+
export interface FilePreviewProps extends ChatbotModalProps {
8+
/** Class applied to modal */
9+
className?: string;
10+
/** Function that handles modal toggle */
11+
handleModalToggle: (event: React.MouseEvent | MouseEvent | KeyboardEvent) => void;
12+
/** Whether modal is open */
13+
isModalOpen: boolean;
14+
/** Title of modal */
15+
title?: string;
16+
/** Display mode for the Chatbot parent; this influences the styles applied */
17+
displayMode?: ChatbotDisplayMode;
18+
/** File name */
19+
fileName: string;
20+
/** Sets modal to compact styling. */
21+
isCompact?: boolean;
22+
/** Additional props passed to modal header */
23+
modalHeaderProps?: ModalHeaderProps;
24+
/** Additional props passed to modal body */
25+
modalBodyProps?: ModalBodyProps;
26+
}
27+
28+
const FilePreview: FunctionComponent<FilePreviewProps> = ({
29+
isModalOpen,
30+
displayMode = ChatbotDisplayMode.default,
31+
children,
32+
fileName,
33+
isCompact,
34+
className,
35+
handleModalToggle,
36+
title = 'File preview',
37+
modalHeaderProps,
38+
modalBodyProps,
39+
...props
40+
}: FilePreviewProps) => (
41+
<ChatbotModal
42+
isOpen={isModalOpen}
43+
className={`pf-chatbot__file-preview-modal pf-chatbot__file-preview-modal--${displayMode} ${isCompact ? 'pf-m-compact' : ''} ${className ? className : ''}`}
44+
displayMode={displayMode}
45+
onClose={handleModalToggle}
46+
isCompact={isCompact}
47+
{...props}
48+
>
49+
<ModalHeader title={title} {...modalHeaderProps} />
50+
<ModalBody className="pf-chatbot__file-preview-body" {...modalBodyProps}>
51+
<FileIcon className="pf-chatbot__file-preview-icon" />
52+
<h2 className="pf-chatbot__file-preview-name">{fileName}</h2>
53+
{children && <div className="pf-chatbot__file-preview-body">{children}</div>}
54+
</ModalBody>
55+
</ChatbotModal>
56+
);
57+
58+
export default FilePreview;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default } from './FilePreview';
2+
3+
export * from './FilePreview';

packages/module/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export * from './FileDetailsLabel';
5454
export { default as FileDropZone } from './FileDropZone';
5555
export * from './FileDropZone';
5656

57+
export { default as FilePreview } from './FilePreview';
58+
export * from './FilePreview';
59+
5760
export { default as LoadingMessage } from './LoadingMessage';
5861
export * from './LoadingMessage';
5962

packages/module/src/main.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
@import './FileDetails/FileDetails';
1616
@import './FileDetailsLabel/FileDetailsLabel';
1717
@import './FileDropZone/FileDropZone';
18+
@import './FilePreview/FilePreview.scss';
1819
@import './Message/Message';
1920
@import './Message/CodeBlockMessage/CodeBlockMessage';
2021
@import './Message/ImageMessage/ImageMessage';

0 commit comments

Comments
 (0)