MM-23523 Migrate "components/markdown_image/*" to typescript (#23524)
Этот коммит содержится в:
@@ -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
|
||||||
Ссылка в новой задаче
Block a user