[MM-47083] Migrate "components/pdf_preview.jsx" and tests to Typescript (#25211)

Этот коммит содержится в:
Sudhanva-Nadiger
2024-01-02 16:03:42 +05:30
коммит произвёл GitHub
родитель b4ee90afca
Коммит 64232a106e
6 изменённых файлов: 91 добавлений и 45 удалений

Просмотреть файл

@@ -12,7 +12,20 @@ exports[`component/PDFPreview should match snapshot, not successful 1`] = `
<Connect(FileInfoPreview) <Connect(FileInfoPreview)
fileInfo={ fileInfo={
Object { Object {
"archived": false,
"clientId": "client_id",
"create_at": 1,
"delete_at": 1,
"extension": "pdf", "extension": "pdf",
"has_preview_image": true,
"height": 200,
"id": "file_info_id",
"mime_type": "mime_type",
"name": "name",
"size": 1,
"update_at": 1,
"user_id": "user_id",
"width": 350,
} }
} }
fileUrl="https://pre-release.mattermost.com/api/v4/files/ips59w4w9jnfbrs3o94m1dbdie" fileUrl="https://pre-release.mattermost.com/api/v4/files/ips59w4w9jnfbrs3o94m1dbdie"

Просмотреть файл

@@ -6,6 +6,8 @@ import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import type {FileInfo} from '@mattermost/types/files';
import {closeModal} from 'actions/views/modals'; import {closeModal} from 'actions/views/modals';
import {isModalOpen} from 'selectors/views/modals'; import {isModalOpen} from 'selectors/views/modals';
@@ -70,8 +72,8 @@ function CloudInvoicePreview(props: Props) {
extension: 'pdf', extension: 'pdf',
size: 0, size: 0,
name: '', name: '',
}} } as FileInfo}
fileUrl={props.url} fileUrl={props.url ?? ''}
scale={1.4} scale={1.4}
handleBgClose={() => {}} handleBgClose={() => {}}
/> />

Просмотреть файл

@@ -15,6 +15,7 @@ import AudioVideoPreview from 'components/audio_video_preview';
import CodePreview from 'components/code_preview'; import CodePreview from 'components/code_preview';
import FileInfoPreview from 'components/file_info_preview'; import FileInfoPreview from 'components/file_info_preview';
import LoadingImagePreview from 'components/loading_image_preview'; import LoadingImagePreview from 'components/loading_image_preview';
import type {Props as PDFPreviewComponentProps} from 'components/pdf_preview';
import Constants, {FileTypes, ZoomSettings} from 'utils/constants'; import Constants, {FileTypes, ZoomSettings} from 'utils/constants';
import * as Keyboard from 'utils/keyboard'; import * as Keyboard from 'utils/keyboard';
@@ -31,7 +32,7 @@ import type {LinkInfo} from './types';
import './file_preview_modal.scss'; import './file_preview_modal.scss';
const PDFPreview = React.lazy(() => import('components/pdf_preview')); const PDFPreview = React.lazy<React.ComponentType<PDFPreviewComponentProps>>(() => import('components/pdf_preview'));
const KeyCodes = Constants.KeyCodes; const KeyCodes = Constants.KeyCodes;
@@ -326,7 +327,7 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
> >
<React.Suspense fallback={null}> <React.Suspense fallback={null}>
<PDFPreview <PDFPreview
fileInfo={fileInfo} fileInfo={fileInfo as FileInfo}
fileUrl={fileUrl} fileUrl={fileUrl}
scale={this.state.scale[this.state.imageIndex]} scale={this.state.scale[this.state.imageIndex]}
handleBgClose={this.handleBgClose} handleBgClose={this.handleBgClose}

Просмотреть файл

@@ -2,23 +2,27 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import type {PDFDocumentProxy, PDFPageProxy} from 'pdfjs-dist';
import React from 'react'; import React from 'react';
import PDFPreview from 'components/pdf_preview.jsx'; import PDFPreview from 'components/pdf_preview';
import type {Props} from 'components/pdf_preview';
import {TestHelper} from 'utils/test_helper';
jest.mock('pdfjs-dist', () => ({ jest.mock('pdfjs-dist', () => ({
getDocument: () => Promise.resolve({ getDocument: () => Promise.resolve({
numPages: 3, numPages: 3,
getPage: (i) => Promise.resolve({ getPage: (i: number) => Promise.resolve({
pageIndex: i, pageIndex: i,
getContext: (s) => Promise.resolve({s}), getContext: (s: string) => Promise.resolve({s}),
}), }),
}), }),
})); }));
describe('component/PDFPreview', () => { describe('component/PDFPreview', () => {
const requiredProps = { const requiredProps: Props = {
fileInfo: {extension: 'pdf'}, fileInfo: TestHelper.getFileInfoMock({extension: 'pdf'}),
fileUrl: 'https://pre-release.mattermost.com/api/v4/files/ips59w4w9jnfbrs3o94m1dbdie', fileUrl: 'https://pre-release.mattermost.com/api/v4/files/ips59w4w9jnfbrs3o94m1dbdie',
scale: 1, scale: 1,
handleBgClose: jest.fn(), handleBgClose: jest.fn(),
@@ -40,7 +44,7 @@ describe('component/PDFPreview', () => {
}); });
test('should update state with new value from props when prop changes', () => { test('should update state with new value from props when prop changes', () => {
const wrapper = shallow( const wrapper = shallow<PDFPreview>(
<PDFPreview {...requiredProps}/>, <PDFPreview {...requiredProps}/>,
); );
const newFileUrl = 'https://some-new-url'; const newFileUrl = 'https://some-new-url';
@@ -51,19 +55,22 @@ describe('component/PDFPreview', () => {
}); });
test('should return correct state when onDocumentLoad is called', () => { test('should return correct state when onDocumentLoad is called', () => {
const wrapper = shallow( const wrapper = shallow<PDFPreview>(
<PDFPreview {...requiredProps}/>, <PDFPreview {...requiredProps}/>,
); );
let pdf = {numPages: 0}; let pdf = {numPages: 0} as PDFDocumentProxy;
wrapper.instance().onDocumentLoad(pdf); wrapper.instance().onDocumentLoad(pdf);
expect(wrapper.state('pdf')).toEqual(pdf); expect(wrapper.state('pdf')).toEqual(pdf);
expect(wrapper.state('numPages')).toEqual(pdf.numPages); expect(wrapper.state('numPages')).toEqual(pdf.numPages);
pdf = { pdf = {
numPages: 100, numPages: 100,
getPage: (i) => Promise.resolve(i), getPage: async (i) => {
}; const page = {pageNumber: i} as PDFPageProxy;
return Promise.resolve(page);
},
} as PDFDocumentProxy;
wrapper.instance().onDocumentLoad(pdf); wrapper.instance().onDocumentLoad(pdf);
expect(wrapper.state('pdf')).toEqual(pdf); expect(wrapper.state('pdf')).toEqual(pdf);
expect(wrapper.state('numPages')).toEqual(pdf.numPages); expect(wrapper.state('numPages')).toEqual(pdf.numPages);

Просмотреть файл

@@ -2,9 +2,12 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import PropTypes from 'prop-types'; import type {PDFDocumentProxy, PDFPageProxy} from 'pdfjs-dist';
import type {RenderParameters} from 'pdfjs-dist/types/src/display/api';
import React from 'react'; import React from 'react';
import type {FileInfo} from '@mattermost/types/files';
import {getFileDownloadUrl} from 'mattermost-redux/utils/file_utils'; import {getFileDownloadUrl} from 'mattermost-redux/utils/file_utils';
import FileInfoPreview from 'components/file_info_preview'; import FileInfoPreview from 'components/file_info_preview';
@@ -14,23 +17,38 @@ import {getSiteURL} from 'utils/url';
const INITIAL_RENDERED_PAGES = 3; const INITIAL_RENDERED_PAGES = 3;
export default class PDFPreview extends React.PureComponent { export type Props = {
static propTypes = {
/** /**
* Compare file types * Compare file types
*/ */
fileInfo: PropTypes.object.isRequired, fileInfo: FileInfo;
/** /**
* URL of pdf file to output and compare to update props url * URL of pdf file to output and compare to update props url
*/ */
fileUrl: PropTypes.string.isRequired, fileUrl: string;
scale: PropTypes.number.isRequired, scale: number;
handleBgClose: PropTypes.func.isRequired, handleBgClose: (e: React.MouseEvent<Element, MouseEvent>) => void;
}; }
constructor(props) { type State = {
pdf: PDFDocumentProxy | null;
pdfPages: Record<number, PDFPageProxy>;
pdfPagesLoaded: Record<number, boolean>;
numPages: number;
loading: boolean;
success: boolean;
prevFileUrl: string;
}
export default class PDFPreview extends React.PureComponent<Props, State> {
public pdfPagesRendered: Record<number, boolean>;
public container: React.RefObject<HTMLDivElement>;
public parentNode: HTMLElement|null = null;
public pdfCanvasRef: {[key: string]: React.RefObject<HTMLCanvasElement>} = {};
constructor(props: Props) {
super(props); super(props);
this.pdfPagesRendered = {}; this.pdfPagesRendered = {};
@@ -38,11 +56,12 @@ export default class PDFPreview extends React.PureComponent {
this.state = { this.state = {
pdf: null, pdf: null,
pdfPages: {}, pdfPages: [],
pdfPagesLoaded: {}, pdfPagesLoaded: {},
numPages: 0, numPages: 0,
loading: true, loading: true,
success: false, success: false,
prevFileUrl: '',
}; };
} }
@@ -50,7 +69,7 @@ export default class PDFPreview extends React.PureComponent {
this.getPdfDocument(); this.getPdfDocument();
if (this.container.current) { if (this.container.current) {
this.parentNode = this.container.current.parentElement; this.parentNode = this.container.current.parentElement;
this.parentNode.addEventListener('scroll', this.handleScroll); this.parentNode?.addEventListener('scroll', this.handleScroll);
} }
} }
@@ -60,7 +79,7 @@ export default class PDFPreview extends React.PureComponent {
} }
} }
static getDerivedStateFromProps(props, state) { static getDerivedStateFromProps(props: Props, state: State) {
if (props.fileUrl !== state.prevFileUrl) { if (props.fileUrl !== state.prevFileUrl) {
return { return {
pdf: null, pdf: null,
@@ -75,7 +94,7 @@ export default class PDFPreview extends React.PureComponent {
return null; return null;
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps: Props, prevState: State) {
if (this.props.fileUrl !== prevProps.fileUrl) { if (this.props.fileUrl !== prevProps.fileUrl) {
this.getPdfDocument(); this.getPdfDocument();
this.pdfPagesRendered = {}; this.pdfPagesRendered = {};
@@ -96,16 +115,16 @@ export default class PDFPreview extends React.PureComponent {
} }
} }
downloadFile = (e) => { downloadFile = (e: React.FormEvent) => {
const fileDownloadUrl = this.props.fileInfo.link || getFileDownloadUrl(this.props.fileInfo.id); const fileDownloadUrl = this.props.fileInfo.link || getFileDownloadUrl(this.props.fileInfo.id);
e.preventDefault(); e.preventDefault();
window.location.href = fileDownloadUrl; window.location.href = fileDownloadUrl;
}; };
isInViewport = (page) => { isInViewport = (page: Element) => {
const bounding = page.getBoundingClientRect(); const bounding = page.getBoundingClientRect();
const viewportTop = this.container.current.scrollTop; const viewportTop = this.container.current?.scrollTop ?? 0;
const viewportBottom = viewportTop + this.container.current.parentElement.clientHeight; const viewportBottom = viewportTop + (this.parentNode?.clientHeight ?? 0);
return ( return (
(bounding.top >= viewportTop && bounding.top <= viewportBottom) || (bounding.top >= viewportTop && bounding.top <= viewportBottom) ||
(bounding.bottom >= viewportTop && bounding.bottom <= viewportBottom) || (bounding.bottom >= viewportTop && bounding.bottom <= viewportBottom) ||
@@ -113,8 +132,8 @@ export default class PDFPreview extends React.PureComponent {
); );
}; };
renderPDFPage = async (pageIndex) => { renderPDFPage = async (pageIndex: number) => {
const canvas = this[`pdfCanvasRef-${pageIndex}`].current; const canvas = this.pdfCanvasRef[`pdfCanvasRef-${pageIndex}`].current;
if (!canvas) { if (!canvas) {
// Refs are undefined when testing // Refs are undefined when testing
return; return;
@@ -130,16 +149,16 @@ export default class PDFPreview extends React.PureComponent {
return; return;
} }
const page = await this.loadPage(this.state.pdf, pageIndex); const page = await this.loadPage(this.state.pdf!, pageIndex);
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
const viewport = page.getViewport({scale: this.props.scale}); const viewport = page.getViewport({scale: this.props.scale});
canvas.height = viewport.height; canvas.height = viewport.height;
canvas.width = viewport.width; canvas.width = viewport.width;
const renderContext = { const renderContext = {
canvasContext: context, canvasContext: context as object,
viewport, viewport,
}; } as RenderParameters;
await page.render(renderContext).promise; await page.render(renderContext).promise;
this.pdfPagesRendered[pageIndex] = true; this.pdfPagesRendered[pageIndex] = true;
@@ -162,20 +181,20 @@ export default class PDFPreview extends React.PureComponent {
} }
}; };
onDocumentLoad = (pdf) => { onDocumentLoad = (pdf: PDFDocumentProxy) => {
this.setState({pdf, numPages: pdf.numPages}); this.setState({pdf, numPages: pdf.numPages});
for (let i = 0; i < pdf.numPages; i++) { for (let i = 0; i < pdf.numPages; i++) {
this[`pdfCanvasRef-${i}`] = React.createRef(); this.pdfCanvasRef[`pdfCanvasRef-${i}`] = React.createRef();
} }
this.setState({loading: false, success: true}); this.setState({loading: false, success: true});
}; };
onDocumentLoadError = (reason) => { onDocumentLoadError = (reason: string) => {
console.log('Unable to load PDF preview: ' + reason); //eslint-disable-line no-console console.log('Unable to load PDF preview: ' + reason); //eslint-disable-line no-console
this.setState({loading: false, success: false}); this.setState({loading: false, success: false});
}; };
loadPage = async (pdf, pageIndex) => { loadPage = async (pdf: PDFDocumentProxy, pageIndex: number) => {
if (this.state.pdfPagesLoaded[pageIndex]) { if (this.state.pdfPagesLoaded[pageIndex]) {
return this.state.pdfPages[pageIndex]; return this.state.pdfPages[pageIndex];
} }
@@ -225,7 +244,7 @@ export default class PDFPreview extends React.PureComponent {
for (let i = 0; i < this.state.numPages; i++) { for (let i = 0; i < this.state.numPages; i++) {
pdfCanvases.push( pdfCanvases.push(
<canvas <canvas
ref={this[`pdfCanvasRef-${i}`]} ref={this.pdfCanvasRef[`pdfCanvasRef-${i}`]}
key={'previewpdfcanvas' + i} key={'previewpdfcanvas' + i}
/>, />,
); );

4
webapp/channels/src/types/external/pdf-worker-entry.d.ts поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,4 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
declare module 'pdfjs-dist/build/pdf.worker.entry.js';