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,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 (
<>
<Checkbox
label="Show compact version"
isChecked={isCompact}
onChange={() => setIsCompact(!isCompact)}
id="modal-compact-no-preview"
name="modal-compact-no-preview"
></Checkbox>
<Button onClick={handleModalToggle}>Launch file preview modal</Button>
<FilePreview
isModalOpen={isModalOpen}
handleModalToggle={handleModalToggle}
fileName="compressed-file.zip"
isCompact={isCompact}
>
Preview unavailable
</FilePreview>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<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)

Expand Down Expand Up @@ -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 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"

```

### Failed attachment error

When an attachment upload fails, a [danger alert](/components/alert) is displayed to provide details about the reason for failure.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 22 additions & 0 deletions packages/module/src/FilePreview/FilePreview.scss
Original file line number Diff line number Diff line change
@@ -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);
}
112 changes: 112 additions & 0 deletions packages/module/src/FilePreview/FilePreview.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<FilePreview {...defaultProps} />);
expect(screen.getByText('File preview')).toBeInTheDocument();
expect(screen.getByText('test-file.txt')).toBeInTheDocument();
});

it('should render with custom title', () => {
const customTitle = 'Custom file preview title';
render(<FilePreview {...defaultProps} title={customTitle} />);
expect(screen.getByRole('heading', { name: customTitle })).toBeTruthy();
});

it('should handle modal toggle when closed', () => {
const mockToggle = jest.fn();
render(<FilePreview {...defaultProps} isModalOpen={false} handleModalToggle={mockToggle} />);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

it('should apply default display mode class', () => {
render(<FilePreview {...defaultProps} />);
const modal = screen.getByRole('dialog');
expect(modal).toHaveClass('pf-chatbot__file-preview-modal--default');
});

it('should apply custom display mode class', () => {
render(<FilePreview {...defaultProps} displayMode={ChatbotDisplayMode.fullscreen} />);
const modal = screen.getByRole('dialog');
expect(modal).toHaveClass('pf-chatbot__file-preview-modal--fullscreen');
});

it('should apply compact styling when isCompact is true', () => {
render(<FilePreview {...defaultProps} isCompact />);
const modal = screen.getByRole('dialog');
expect(modal).toHaveClass('pf-m-compact');
});

it('should not apply compact styling when isCompact is false', () => {
render(<FilePreview {...defaultProps} isCompact={false} />);
const modal = screen.getByRole('dialog');
expect(modal).not.toHaveClass('pf-m-compact');
});

it('should apply custom className', () => {
const customClass = 'custom-file-preview';
render(<FilePreview {...defaultProps} className={customClass} />);
const modal = screen.getByRole('dialog');
expect(modal).toHaveClass(customClass);
});

it('should pass through additional props to ChatbotModal', () => {
render(<FilePreview {...defaultProps} data-testid="file-preview-modal" />);
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(<FilePreview {...defaultProps} modalHeaderProps={modalHeaderProps} />);
const header = screen.getByTestId('custom-header');
expect(header).toBeInTheDocument();
});

it('should pass modalBodyProps to ModalBody', () => {
const modalBodyProps = {
'data-testid': 'custom-body'
} as ModalBodyProps;
render(<FilePreview {...defaultProps} modalBodyProps={modalBodyProps} />);
const body = screen.getByTestId('custom-body');
expect(body).toBeInTheDocument();
});

it('should pass ouiaId to ChatbotModal', () => {
const ouiaId = 'file-preview-ouia-id';
render(<FilePreview {...defaultProps} ouiaId={ouiaId} />);
const modal = screen.getByRole('dialog');
expect(modal).toHaveAttribute('data-ouia-component-id', ouiaId);
});

it('should handle complex children', () => {
const complexChildren = (
<div>
<h3>File details</h3>
<p>Size: 1.2 MB</p>
<Button>Download</Button>
</div>
);
render(<FilePreview {...defaultProps}>{complexChildren}</FilePreview>);
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();
});
});
58 changes: 58 additions & 0 deletions packages/module/src/FilePreview/FilePreview.tsx
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +10 to +13

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment as the other PR regarding the prop names being onClose and isOpen instead

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping this only because it would cause a difference with our other modals' API. We'd have to change all of them imho.

/** 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<FilePreviewProps> = ({
isModalOpen,
displayMode = ChatbotDisplayMode.default,
children,
fileName,
isCompact,
className,
handleModalToggle,
title = 'File preview',
modalHeaderProps,
modalBodyProps,
...props
}: FilePreviewProps) => (
<ChatbotModal
isOpen={isModalOpen}
className={`pf-chatbot__file-preview-modal pf-chatbot__file-preview-modal--${displayMode} ${isCompact ? 'pf-m-compact' : ''} ${className ? className : ''}`}
displayMode={displayMode}
onClose={handleModalToggle}
isCompact={isCompact}
{...props}
>
<ModalHeader title={title} {...modalHeaderProps} />
<ModalBody className="pf-chatbot__file-preview-body" {...modalBodyProps}>
<FileIcon className="pf-chatbot__file-preview-icon" />
<h2 className="pf-chatbot__file-preview-name">{fileName}</h2>
{children && <div className="pf-chatbot__file-preview-body">{children}</div>}
</ModalBody>
</ChatbotModal>
);

export default FilePreview;
3 changes: 3 additions & 0 deletions packages/module/src/FilePreview/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default } from './FilePreview';

export * from './FilePreview';
3 changes: 3 additions & 0 deletions packages/module/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
1 change: 1 addition & 0 deletions packages/module/src/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading