From 16606f44205d7da2386e1ce6dcbe5cb8e569f6b4 Mon Sep 17 00:00:00 2001 From: Sai Deepesh Date: Fri, 21 Jul 2023 20:30:19 +0530 Subject: [PATCH] MM-23523 Migrate "components/markdown_image/*" to typescript (#23524) --- ....jsx.snap => markdown_image.test.tsx.snap} | 28 ++++-- .../markdown_image/{index.js => index.ts} | 9 +- ...image.test.jsx => markdown_image.test.tsx} | 50 ++++++++--- ...{markdown_image.jsx => markdown_image.tsx} | 86 +++++++++++-------- 4 files changed, 115 insertions(+), 58 deletions(-) rename webapp/channels/src/components/markdown_image/__snapshots__/{markdown_image.test.jsx.snap => markdown_image.test.tsx.snap} (90%) rename webapp/channels/src/components/markdown_image/{index.js => index.ts} (50%) rename webapp/channels/src/components/markdown_image/{markdown_image.test.jsx => markdown_image.test.tsx} (89%) rename webapp/channels/src/components/markdown_image/{markdown_image.jsx => markdown_image.tsx} (81%) diff --git a/webapp/channels/src/components/markdown_image/__snapshots__/markdown_image.test.jsx.snap b/webapp/channels/src/components/markdown_image/__snapshots__/markdown_image.test.tsx.snap similarity index 90% rename from webapp/channels/src/components/markdown_image/__snapshots__/markdown_image.test.jsx.snap rename to webapp/channels/src/components/markdown_image/__snapshots__/markdown_image.test.tsx.snap index 6e731382ba..519c46631a 100644 --- a/webapp/channels/src/components/markdown_image/__snapshots__/markdown_image.test.jsx.snap +++ b/webapp/channels/src/components/markdown_image/__snapshots__/markdown_image.test.tsx.snap @@ -5,7 +5,7 @@ exports[`components/MarkdownImage should match snapshot 1`] = ` imageMetadata={ Object { "format": "png", - "frame_count": 0, + "frameCount": 0, "height": 90, "width": 1041, } @@ -23,22 +23,32 @@ exports[`components/MarkdownImage should match snapshot for SizeAwareImage dimen dimensions={ Object { "format": "jpg", - "frame_count": 0, + "frameCount": 0, "height": 90, "width": 100, } } + height="" onClick={[Function]} onImageLoadFail={[Function]} onImageLoaded={[Function]} showLoader={false} src="safeSrc" + title="test title" + width="" /> `; exports[`components/MarkdownImage should match snapshot for broken link 1`] = ` @@ -53,7 +63,7 @@ exports[`components/MarkdownImage should provide image src as an alt text for Ma postId="post_id" > `; @@ -115,16 +128,19 @@ exports[`components/MarkdownImage should render an image with preview modal if t dimensions={ Object { "format": "png", - "frame_count": 0, + "frameCount": 0, "height": 90, "width": 1041, } } + height="" onClick={[Function]} onImageLoadFail={[Function]} onImageLoaded={[Function]} showLoader={false} src="https://example.com/image.png" + title="test title" + width="" /> `; diff --git a/webapp/channels/src/components/markdown_image/index.js b/webapp/channels/src/components/markdown_image/index.ts similarity index 50% rename from webapp/channels/src/components/markdown_image/index.js rename to webapp/channels/src/components/markdown_image/index.ts index c9741e0ffb..bd7b33fcdb 100644 --- a/webapp/channels/src/components/markdown_image/index.js +++ b/webapp/channels/src/components/markdown_image/index.ts @@ -2,15 +2,16 @@ // See LICENSE.txt for license information. import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; +import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux'; +import {Action, GenericAction} from 'mattermost-redux/types/actions'; import {openModal} from 'actions/views/modals'; -import MarkdownImage from './markdown_image'; +import MarkdownImage, {Props} from './markdown_image'; -function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch: Dispatch) { return { - actions: bindActionCreators({ + actions: bindActionCreators, Props['actions']>({ openModal, }, dispatch), }; diff --git a/webapp/channels/src/components/markdown_image/markdown_image.test.jsx b/webapp/channels/src/components/markdown_image/markdown_image.test.tsx similarity index 89% rename from webapp/channels/src/components/markdown_image/markdown_image.test.jsx rename to webapp/channels/src/components/markdown_image/markdown_image.test.tsx index bf7e54cb75..d2b3b5ca58 100644 --- a/webapp/channels/src/components/markdown_image/markdown_image.test.jsx +++ b/webapp/channels/src/components/markdown_image/markdown_image.test.tsx @@ -18,13 +18,18 @@ describe('components/MarkdownImage', () => { format: 'png', height: 90, width: 1041, - frame_count: 0, + frameCount: 0, }, alt: 'test image', + height: '', + width: '', + title: 'test title', className: 'markdown-inline-img', postId: 'post_id', imageIsLink: false, onImageLoaded: jest.fn(), + onImageHeightChanged: jest.fn(), + postType: 'system_generic', actions: { openModal: jest.fn(), }, @@ -40,7 +45,14 @@ describe('components/MarkdownImage', () => { }); test('should match snapshot for broken link', () => { - const props = {...baseProps, imageMetadata: {}, src: 'brokenLink'}; + const props = {...baseProps, + imageMetadata: { + format: 'png', + height: 10, + width: 10, + frameCount: 0, + }, + src: 'brokenLink'}; const wrapper = shallow( , ); @@ -49,20 +61,36 @@ describe('components/MarkdownImage', () => { }); test('should handle load failure properly', () => { - const props = {...baseProps, imageMetadata: {}, src: 'brokenLink'}; + const props = {...baseProps, + imageMetadata: { + format: 'png', + height: 10, + width: 10, + frameCount: 0, + + }, + src: 'brokenLink'}; const wrapper = shallow( , ); expect(wrapper.state('loadFailed')).toBe(false); - wrapper.instance().handleLoadFail(); + (wrapper.instance() as MarkdownImage).handleLoadFail(); expect(wrapper.state('loadFailed')).toBe(true); }); test('should reset loadFailed state after image source is updated', () => { - const props = {...baseProps, imageMetadata: {}, src: 'brokenLink'}; + const props = {...baseProps, + imageMetadata: { + format: 'png', + height: 10, + width: 10, + frameCount: 0, + + }, + src: 'brokenLink'}; const nextProps = {...baseProps, src: 'https://example.com/image.png'}; const wrapper = shallow( , @@ -133,7 +161,7 @@ describe('components/MarkdownImage', () => { expect(wrapper.state('loaded')).toBe(false); - wrapper.instance().handleImageLoaded(dimensions); + (wrapper.instance() as MarkdownImage).handleImageLoaded(dimensions); expect(wrapper.state('loaded')).toBe(true); @@ -143,7 +171,7 @@ describe('components/MarkdownImage', () => { it('should match snapshot for SizeAwareImage dimensions', () => { const props = {...baseProps, - imageMetadata: {format: 'jpg', frame_count: 0, width: 100, height: 90}, + imageMetadata: {format: 'jpg', frameCount: 0, width: 100, height: 90}, src: 'path/image', }; const wrapper = shallow( @@ -198,8 +226,10 @@ describe('components/MarkdownImage', () => { const wrapper = shallow( , ); - - wrapper.instance().showModal({preventDefault: () => {}}, 'https://example.com/image.png'); + const mockEvent = { + preventDefault: () => {}, + }; + (wrapper.instance() as MarkdownImage).showModal(mockEvent as unknown as React.MouseEvent, 'https://example.com/image.png'); expect(props.actions.openModal).toHaveBeenCalledTimes(1); }); @@ -281,7 +311,7 @@ describe('components/MarkdownImage', () => { test('should provide image src as an alt text for MarkdownImageExpand if image has no own alt text', () => { const props = { - alt: null, + alt: '', title: 'test title', className: 'markdown-inline-img', postId: 'post_id', diff --git a/webapp/channels/src/components/markdown_image/markdown_image.jsx b/webapp/channels/src/components/markdown_image/markdown_image.tsx similarity index 81% rename from webapp/channels/src/components/markdown_image/markdown_image.jsx rename to webapp/channels/src/components/markdown_image/markdown_image.tsx index 2e5f570031..93dbc4afe6 100644 --- a/webapp/channels/src/components/markdown_image/markdown_image.jsx +++ b/webapp/channels/src/components/markdown_image/markdown_image.tsx @@ -1,48 +1,53 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import PropTypes from 'prop-types'; -import React from 'react'; +import React, {PureComponent, MouseEvent} from 'react'; -import Constants, {ModalIdentifiers} from 'utils/constants'; +import {Post, PostImage} from '@mattermost/types/posts'; + +import {ModalData} from 'types/actions'; import MarkdownImageExpand from 'components/markdown_image_expand'; import ExternalImage from 'components/external_image'; import SizeAwareImage from 'components/size_aware_image'; import FilePreviewModal from 'components/file_preview_modal'; - -import brokenImageIcon from 'images/icons/brokenimage.png'; import ExternalLink from 'components/external_link'; -export default class MarkdownImage extends React.PureComponent { - static defaultProps = { - imageMetadata: {}, +import brokenImageIcon from 'images/icons/brokenimage.png'; +import Constants, {ModalIdentifiers} from 'utils/constants'; + +export type Props = { + alt: string; + imageMetadata?: PostImage; + src: string; + + // height and width come from the Markdown renderer as either "auto" or a string containing a number. + height: string; + width: string; + title: string; + className: string; + postId: Post['id']; + imageIsLink: boolean; + onImageLoaded?: ({height, width}: {height: number; width: number}) => void; + onImageHeightChanged?: (isExpanded: boolean) => void; + postType?: string; + actions: { + openModal:

(modalData: ModalData

) => void; + }; + hideUtilities?: boolean; +}; + +type State = { + loadFailed: boolean; + loaded: boolean; +}; + +export default class MarkdownImage extends PureComponent { + static defaultProps: Partial = { + imageMetadata: {} as PostImage, }; - static propTypes = { - alt: PropTypes.string, - imageMetadata: PropTypes.object, - src: PropTypes.string.isRequired, - - // height and width come from the Markdown renderer as either "auto" or a string containing a number. - height: PropTypes.string, - width: PropTypes.string, - - title: PropTypes.string, - className: PropTypes.string.isRequired, - postId: PropTypes.string.isRequired, - imageIsLink: PropTypes.bool.isRequired, - onImageLoaded: PropTypes.func, - onImageHeightChanged: PropTypes.func, - postType: PropTypes.string, - - actions: PropTypes.shape({ - openModal: PropTypes.func, - }).isRequired, - hideUtilities: PropTypes.bool, - }; - - constructor(props) { + constructor(props: Props) { super(props); this.state = { @@ -58,6 +63,10 @@ export default class MarkdownImage extends React.PureComponent { width, } = this.props; + if (!imageMetadata) { + return 0; + } + if (!height) { return imageMetadata.height; } @@ -71,12 +80,12 @@ export default class MarkdownImage extends React.PureComponent { return parseInt(height, 10); }; - getFileExtensionFromUrl = (url) => { + getFileExtensionFromUrl = (url: string) => { const index = url.lastIndexOf('.'); return index > 0 ? url.substring(index + 1) : null; }; - showModal = (e, link) => { + showModal = (e: MouseEvent, link: string) => { const extension = this.getFileExtensionFromUrl(link); if (!this.props.imageIsLink && extension) { @@ -86,11 +95,12 @@ export default class MarkdownImage extends React.PureComponent { modalId: ModalIdentifiers.FILE_PREVIEW_MODAL, dialogType: FilePreviewModal, dialogProps: { + startIndex: 0, postId: this.props.postId, fileInfos: [{ has_preview_image: false, link, - extension: this.props.imageMetadata.format || extension, + extension: this.props?.imageMetadata?.format ?? extension, name: this.props.alt, }], }, @@ -107,17 +117,17 @@ export default class MarkdownImage extends React.PureComponent { this.props.postType === Constants.PostTypes.HEADER_CHANGE; }; - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { this.onUpdated(prevProps.src); } - onUpdated = (prevSrc) => { + onUpdated = (prevSrc: string) => { if (this.props.src && this.props.src !== prevSrc) { this.setState({loadFailed: false}); } }; - handleImageLoaded = ({height, width}) => { + handleImageLoaded = ({height, width}: {height: number; width: number}) => { this.setState({ loaded: true, }, () => { // Call onImageLoaded prop only after state has already been set