MM-23523 Migrate "components/markdown_image/*" to typescript (#23524)

Этот коммит содержится в:
Sai Deepesh
2023-07-21 20:30:19 +05:30
коммит произвёл GitHub
родитель ab38a8fbd7
Коммит 16606f4420
4 изменённых файлов: 115 добавлений и 58 удалений

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

@@ -5,7 +5,7 @@ exports[`components/MarkdownImage should match snapshot 1`] = `
imageMetadata={ imageMetadata={
Object { Object {
"format": "png", "format": "png",
"frame_count": 0, "frameCount": 0,
"height": 90, "height": 90,
"width": 1041, "width": 1041,
} }
@@ -23,22 +23,32 @@ exports[`components/MarkdownImage should match snapshot for SizeAwareImage dimen
dimensions={ dimensions={
Object { Object {
"format": "jpg", "format": "jpg",
"frame_count": 0, "frameCount": 0,
"height": 90, "height": 90,
"width": 100, "width": 100,
} }
} }
height=""
onClick={[Function]} onClick={[Function]}
onImageLoadFail={[Function]} onImageLoadFail={[Function]}
onImageLoaded={[Function]} onImageLoaded={[Function]}
showLoader={false} showLoader={false}
src="safeSrc" src="safeSrc"
title="test title"
width=""
/> />
`; `;
exports[`components/MarkdownImage should match snapshot for broken link 1`] = ` exports[`components/MarkdownImage should match snapshot for broken link 1`] = `
<Connect(ExternalImage) <Connect(ExternalImage)
imageMetadata={Object {}} imageMetadata={
Object {
"format": "png",
"frameCount": 0,
"height": 10,
"width": 10,
}
}
src="brokenLink" src="brokenLink"
> >
<Component /> <Component />
@@ -53,7 +63,7 @@ exports[`components/MarkdownImage should provide image src as an alt text for Ma
postId="post_id" postId="post_id"
> >
<SizeAwareImage <SizeAwareImage
alt={null} alt=""
className="markdown-inline-img markdown-inline-img--hover cursor--pointer a11y--active" className="markdown-inline-img markdown-inline-img--hover cursor--pointer a11y--active"
dimensions={Object {}} dimensions={Object {}}
height="250" height="250"
@@ -93,16 +103,19 @@ exports[`components/MarkdownImage should render an image with no preview if the
dimensions={ dimensions={
Object { Object {
"format": "png", "format": "png",
"frame_count": 0, "frameCount": 0,
"height": 90, "height": 90,
"width": 1041, "width": 1041,
} }
} }
height=""
onClick={[Function]} onClick={[Function]}
onImageLoadFail={[Function]} onImageLoadFail={[Function]}
onImageLoaded={[Function]} onImageLoaded={[Function]}
showLoader={false} showLoader={false}
src="https://example.com/image.png" src="https://example.com/image.png"
title="test title"
width=""
/> />
</div> </div>
`; `;
@@ -115,16 +128,19 @@ exports[`components/MarkdownImage should render an image with preview modal if t
dimensions={ dimensions={
Object { Object {
"format": "png", "format": "png",
"frame_count": 0, "frameCount": 0,
"height": 90, "height": 90,
"width": 1041, "width": 1041,
} }
} }
height=""
onClick={[Function]} onClick={[Function]}
onImageLoadFail={[Function]} onImageLoadFail={[Function]}
onImageLoaded={[Function]} onImageLoaded={[Function]}
showLoader={false} showLoader={false}
src="https://example.com/image.png" src="https://example.com/image.png"
title="test title"
width=""
/> />
</div> </div>
`; `;

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

@@ -2,15 +2,16 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {connect} from 'react-redux'; 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 {openModal} from 'actions/views/modals';
import MarkdownImage from './markdown_image'; import MarkdownImage, {Props} from './markdown_image';
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
return { return {
actions: bindActionCreators({ actions: bindActionCreators<ActionCreatorsMapObject<Action>, Props['actions']>({
openModal, openModal,
}, dispatch), }, dispatch),
}; };

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

@@ -18,13 +18,18 @@ describe('components/MarkdownImage', () => {
format: 'png', format: 'png',
height: 90, height: 90,
width: 1041, width: 1041,
frame_count: 0, frameCount: 0,
}, },
alt: 'test image', alt: 'test image',
height: '',
width: '',
title: 'test title',
className: 'markdown-inline-img', className: 'markdown-inline-img',
postId: 'post_id', postId: 'post_id',
imageIsLink: false, imageIsLink: false,
onImageLoaded: jest.fn(), onImageLoaded: jest.fn(),
onImageHeightChanged: jest.fn(),
postType: 'system_generic',
actions: { actions: {
openModal: jest.fn(), openModal: jest.fn(),
}, },
@@ -40,7 +45,14 @@ describe('components/MarkdownImage', () => {
}); });
test('should match snapshot for broken link', () => { 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( const wrapper = shallow(
<MarkdownImage {...props}/>, <MarkdownImage {...props}/>,
); );
@@ -49,20 +61,36 @@ describe('components/MarkdownImage', () => {
}); });
test('should handle load failure properly', () => { 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( const wrapper = shallow(
<MarkdownImage {...props}/>, <MarkdownImage {...props}/>,
); );
expect(wrapper.state('loadFailed')).toBe(false); expect(wrapper.state('loadFailed')).toBe(false);
wrapper.instance().handleLoadFail(); (wrapper.instance() as MarkdownImage).handleLoadFail();
expect(wrapper.state('loadFailed')).toBe(true); expect(wrapper.state('loadFailed')).toBe(true);
}); });
test('should reset loadFailed state after image source is updated', () => { 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 nextProps = {...baseProps, src: 'https://example.com/image.png'};
const wrapper = shallow( const wrapper = shallow(
<MarkdownImage {...props}/>, <MarkdownImage {...props}/>,
@@ -133,7 +161,7 @@ describe('components/MarkdownImage', () => {
expect(wrapper.state('loaded')).toBe(false); expect(wrapper.state('loaded')).toBe(false);
wrapper.instance().handleImageLoaded(dimensions); (wrapper.instance() as MarkdownImage).handleImageLoaded(dimensions);
expect(wrapper.state('loaded')).toBe(true); expect(wrapper.state('loaded')).toBe(true);
@@ -143,7 +171,7 @@ describe('components/MarkdownImage', () => {
it('should match snapshot for SizeAwareImage dimensions', () => { it('should match snapshot for SizeAwareImage dimensions', () => {
const props = {...baseProps, 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', src: 'path/image',
}; };
const wrapper = shallow( const wrapper = shallow(
@@ -198,8 +226,10 @@ describe('components/MarkdownImage', () => {
const wrapper = shallow( const wrapper = shallow(
<MarkdownImage {...props}/>, <MarkdownImage {...props}/>,
); );
const mockEvent = {
wrapper.instance().showModal({preventDefault: () => {}}, 'https://example.com/image.png'); preventDefault: () => {},
};
(wrapper.instance() as MarkdownImage).showModal(mockEvent as unknown as React.MouseEvent<HTMLImageElement>, 'https://example.com/image.png');
expect(props.actions.openModal).toHaveBeenCalledTimes(1); 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', () => { test('should provide image src as an alt text for MarkdownImageExpand if image has no own alt text', () => {
const props = { const props = {
alt: null, alt: '',
title: 'test title', title: 'test title',
className: 'markdown-inline-img', className: 'markdown-inline-img',
postId: 'post_id', postId: 'post_id',

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

@@ -1,48 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import PropTypes from 'prop-types'; import React, {PureComponent, MouseEvent} from 'react';
import React 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 MarkdownImageExpand from 'components/markdown_image_expand';
import ExternalImage from 'components/external_image'; import ExternalImage from 'components/external_image';
import SizeAwareImage from 'components/size_aware_image'; import SizeAwareImage from 'components/size_aware_image';
import FilePreviewModal from 'components/file_preview_modal'; import FilePreviewModal from 'components/file_preview_modal';
import brokenImageIcon from 'images/icons/brokenimage.png';
import ExternalLink from 'components/external_link'; import ExternalLink from 'components/external_link';
export default class MarkdownImage extends React.PureComponent { import brokenImageIcon from 'images/icons/brokenimage.png';
static defaultProps = { import Constants, {ModalIdentifiers} from 'utils/constants';
imageMetadata: {},
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: <P>(modalData: ModalData<P>) => void;
};
hideUtilities?: boolean;
};
type State = {
loadFailed: boolean;
loaded: boolean;
};
export default class MarkdownImage extends PureComponent<Props, State> {
static defaultProps: Partial<Props> = {
imageMetadata: {} as PostImage,
}; };
static propTypes = { constructor(props: Props) {
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) {
super(props); super(props);
this.state = { this.state = {
@@ -58,6 +63,10 @@ export default class MarkdownImage extends React.PureComponent {
width, width,
} = this.props; } = this.props;
if (!imageMetadata) {
return 0;
}
if (!height) { if (!height) {
return imageMetadata.height; return imageMetadata.height;
} }
@@ -71,12 +80,12 @@ export default class MarkdownImage extends React.PureComponent {
return parseInt(height, 10); return parseInt(height, 10);
}; };
getFileExtensionFromUrl = (url) => { getFileExtensionFromUrl = (url: string) => {
const index = url.lastIndexOf('.'); const index = url.lastIndexOf('.');
return index > 0 ? url.substring(index + 1) : null; return index > 0 ? url.substring(index + 1) : null;
}; };
showModal = (e, link) => { showModal = (e: MouseEvent<HTMLImageElement>, link: string) => {
const extension = this.getFileExtensionFromUrl(link); const extension = this.getFileExtensionFromUrl(link);
if (!this.props.imageIsLink && extension) { if (!this.props.imageIsLink && extension) {
@@ -86,11 +95,12 @@ export default class MarkdownImage extends React.PureComponent {
modalId: ModalIdentifiers.FILE_PREVIEW_MODAL, modalId: ModalIdentifiers.FILE_PREVIEW_MODAL,
dialogType: FilePreviewModal, dialogType: FilePreviewModal,
dialogProps: { dialogProps: {
startIndex: 0,
postId: this.props.postId, postId: this.props.postId,
fileInfos: [{ fileInfos: [{
has_preview_image: false, has_preview_image: false,
link, link,
extension: this.props.imageMetadata.format || extension, extension: this.props?.imageMetadata?.format ?? extension,
name: this.props.alt, name: this.props.alt,
}], }],
}, },
@@ -107,17 +117,17 @@ export default class MarkdownImage extends React.PureComponent {
this.props.postType === Constants.PostTypes.HEADER_CHANGE; this.props.postType === Constants.PostTypes.HEADER_CHANGE;
}; };
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: Props) {
this.onUpdated(prevProps.src); this.onUpdated(prevProps.src);
} }
onUpdated = (prevSrc) => { onUpdated = (prevSrc: string) => {
if (this.props.src && this.props.src !== prevSrc) { if (this.props.src && this.props.src !== prevSrc) {
this.setState({loadFailed: false}); this.setState({loadFailed: false});
} }
}; };
handleImageLoaded = ({height, width}) => { handleImageLoaded = ({height, width}: {height: number; width: number}) => {
this.setState({ this.setState({
loaded: true, loaded: true,
}, () => { // Call onImageLoaded prop only after state has already been set }, () => { // Call onImageLoaded prop only after state has already been set