[MM-47086] Migrate "components/size_aware_image.jsx" and tests to Typescript (#25230)

Этот коммит содержится в:
Sudhanva-Nadiger
2023-12-30 18:22:27 +05:30
коммит произвёл GitHub
родитель fb1038548e
Коммит ef0c5e2125
9 изменённых файлов: 142 добавлений и 109 удалений

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

@@ -41,10 +41,10 @@ exports[`components/SizeAwareImage should load download and copy link buttons wh
onKeyDown={[Function]} onKeyDown={[Function]}
onLoad={[Function]} onLoad={[Function]}
src="https://example.com/image.png" src="https://example.com/image.png"
tabIndex="0" tabIndex={0}
/> />
<span <span
className="image-preview-utility-buttons-container" className="image-preview-utility-buttons-container image-preview-utility-buttons-container--small-image"
> >
<OverlayTrigger <OverlayTrigger
className="hidden-xs" className="hidden-xs"
@@ -167,10 +167,10 @@ exports[`components/SizeAwareImage should match snapshot when handleSmallImageCo
onKeyDown={[Function]} onKeyDown={[Function]}
onLoad={[Function]} onLoad={[Function]}
src="https://example.com/image.png" src="https://example.com/image.png"
tabIndex="0" tabIndex={0}
/> />
<span <span
className="image-preview-utility-buttons-container" className="image-preview-utility-buttons-container image-preview-utility-buttons-container--small-image"
> >
<OverlayTrigger <OverlayTrigger
className="hidden-xs" className="hidden-xs"
@@ -306,10 +306,10 @@ exports[`components/SizeAwareImage should render a placeholder and has loader wh
onKeyDown={[Function]} onKeyDown={[Function]}
onLoad={[Function]} onLoad={[Function]}
src="https://example.com/image.png" src="https://example.com/image.png"
tabIndex="0" tabIndex={0}
/> />
<span <span
className="image-preview-utility-buttons-container" className="image-preview-utility-buttons-container image-preview-utility-buttons-container--small-image"
> >
<OverlayTrigger <OverlayTrigger
className="hidden-xs" className="hidden-xs"

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

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {PureComponent} from 'react'; import React, {PureComponent} from 'react';
import type {MouseEvent} from 'react'; import type {KeyboardEvent, MouseEvent} from 'react';
import type {Post, PostImage} from '@mattermost/types/posts'; import type {Post, PostImage} from '@mattermost/types/posts';
@@ -86,7 +86,7 @@ export default class MarkdownImage extends PureComponent<Props, State> {
return index > 0 ? url.substring(index + 1) : null; return index > 0 ? url.substring(index + 1) : null;
}; };
showModal = (e: MouseEvent<HTMLImageElement>, link: string) => { showModal = (e: KeyboardEvent<HTMLImageElement> | MouseEvent<HTMLElement>, link = '') => {
const extension = this.getFileExtensionFromUrl(link); const extension = this.getFileExtensionFromUrl(link);
if (!this.props.imageIsLink && extension) { if (!this.props.imageIsLink && extension) {

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

@@ -114,7 +114,7 @@ describe('components/post_view/MessageAttachment', () => {
<MessageAttachment {...props}/>, <MessageAttachment {...props}/>,
); );
wrapper.instance().showModal({preventDefault: () => {}}, 'https://example.com/image.png'); wrapper.instance().showModal({preventDefault: () => {}} as unknown as React.KeyboardEvent<HTMLImageElement> | React.MouseEvent<HTMLElement, MouseEvent>, 'https://example.com/image.png');
expect(props.actions.openModal).toHaveBeenCalledTimes(1); expect(props.actions.openModal).toHaveBeenCalledTimes(1);
}); });

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

@@ -3,7 +3,7 @@
import truncate from 'lodash/truncate'; import truncate from 'lodash/truncate';
import React from 'react'; import React from 'react';
import type {CSSProperties} from 'react'; import type {KeyboardEvent, MouseEvent, CSSProperties} from 'react';
import type {PostAction, PostActionOption} from '@mattermost/types/integration_actions'; import type {PostAction, PostActionOption} from '@mattermost/types/integration_actions';
import type { import type {
@@ -312,14 +312,14 @@ export default class MessageAttachment extends React.PureComponent<Props, State>
); );
}; };
handleFormattedTextClick = (e: React.MouseEvent) => Utils.handleFormattedTextClick(e, this.props.currentRelativeTeamUrl); handleFormattedTextClick = (e: MouseEvent) => Utils.handleFormattedTextClick(e, this.props.currentRelativeTeamUrl);
getFileExtensionFromUrl = (url: string) => { 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: {preventDefault: () => void}, link: string) => { showModal = (e: (KeyboardEvent<HTMLImageElement> | MouseEvent<HTMLElement>), link = '') => {
e.preventDefault(); e.preventDefault();
const extension = this.getFileExtensionFromUrl(link); const extension = this.getFileExtensionFromUrl(link);

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

@@ -2,6 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React from 'react';
import type {KeyboardEvent, MouseEvent} from 'react';
import type {Post, PostImage as PostImageMetadata} from '@mattermost/types/posts'; import type {Post, PostImage as PostImageMetadata} from '@mattermost/types/posts';
@@ -23,7 +24,7 @@ interface Props {
} }
export default class PostImage extends React.PureComponent<Props> { export default class PostImage extends React.PureComponent<Props> {
showModal = (e: React.MouseEvent, link: string) => { showModal = (e: (KeyboardEvent<HTMLImageElement> | MouseEvent<HTMLElement>), link = '') => {
e.preventDefault(); e.preventDefault();
this.props.actions.openModal({ this.props.actions.openModal({

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

@@ -85,7 +85,7 @@ describe('components/SingleImageView', () => {
<SingleImageView {...baseProps}/>, <SingleImageView {...baseProps}/>,
); );
expect(wrapper.state('loaded')).toEqual(false); expect(wrapper.state('loaded')).toEqual(false);
wrapper.find(SizeAwareImage).prop('onImageLoaded')(); wrapper.find(SizeAwareImage).prop('onImageLoaded')?.({height: 0, width: 0});
expect(wrapper.state('loaded')).toEqual(true); expect(wrapper.state('loaded')).toEqual(true);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });

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

@@ -3,6 +3,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import type {KeyboardEvent, MouseEvent} from 'react';
import type {FileInfo} from '@mattermost/types/files'; import type {FileInfo} from '@mattermost/types/files';
@@ -82,7 +83,7 @@ export default class SingleImageView extends React.PureComponent<Props, State> {
} }
}; };
handleImageClick = (e: React.MouseEvent<HTMLDivElement>) => { handleImageClick = (e: (KeyboardEvent<HTMLImageElement> | MouseEvent<HTMLDivElement | HTMLImageElement>)) => {
e.preventDefault(); e.preventDefault();
this.props.actions.openModal({ this.props.actions.openModal({

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

@@ -7,11 +7,13 @@ import {Provider} from 'react-redux';
import LoadingImagePreview from 'components/loading_image_preview'; import LoadingImagePreview from 'components/loading_image_preview';
import SizeAwareImage from 'components/size_aware_image'; import SizeAwareImage from 'components/size_aware_image';
import type {Props} from 'components/size_aware_image';
import mockStore from 'tests/test_store'; import mockStore from 'tests/test_store';
import {TestHelper} from 'utils/test_helper';
describe('components/SizeAwareImage', () => { describe('components/SizeAwareImage', () => {
const baseProps = { const baseProps: Props = {
dimensions: { dimensions: {
height: 200, height: 200,
width: 300, width: 300,
@@ -21,9 +23,9 @@ describe('components/SizeAwareImage', () => {
getFilePublicLink: jest.fn().mockReturnValue(Promise.resolve({data: {link: 'https://example.com/image.png'}})), getFilePublicLink: jest.fn().mockReturnValue(Promise.resolve({data: {link: 'https://example.com/image.png'}})),
src: 'https://example.com/image.png', src: 'https://example.com/image.png',
className: 'class', className: 'class',
fileInfo: { fileInfo: TestHelper.getFileInfoMock({
name: 'photo-1533709752211-118fcaf03312', name: 'photo-1533709752211-118fcaf03312',
}, }),
enablePublicLink: true, enablePublicLink: true,
}; };
@@ -69,11 +71,11 @@ describe('components/SizeAwareImage', () => {
test('should render a mini preview when showLoader is true and preview is set', () => { test('should render a mini preview when showLoader is true and preview is set', () => {
const props = { const props = {
...baseProps, ...baseProps,
fileInfo: { fileInfo: TestHelper.getFileInfoMock({
...baseProps.fileInfo, ...baseProps.fileInfo,
mime_type: 'mime_type', mime_type: 'mime_type',
mini_preview: 'mini_preview', mini_preview: 'mini_preview',
}, }),
}; };
const wrapper = mount(<Provider store={store}><SizeAwareImage {...props}/></Provider>); const wrapper = mount(<Provider store={store}><SizeAwareImage {...props}/></Provider>);
@@ -108,17 +110,22 @@ describe('components/SizeAwareImage', () => {
const height = 123; const height = 123;
const width = 1234; const width = 1234;
const wrapper = shallow(<SizeAwareImage {...baseProps}/>); const wrapper = shallow<SizeAwareImage>(<SizeAwareImage {...baseProps}/>);
wrapper.find('img').prop('onLoad')({target: {naturalHeight: height, naturalWidth: width}}); wrapper.find('img')?.prop('onLoad')?.({target: {naturalHeight: height, naturalWidth: width}} as unknown as React.SyntheticEvent<HTMLImageElement>);
expect(wrapper.state('loaded')).toBe(true); expect(wrapper.state('loaded')).toBe(true);
expect(baseProps.onImageLoaded).toHaveBeenCalledWith({height, width}); expect(baseProps.onImageLoaded).toHaveBeenCalledWith({height, width});
}); });
test('should call onImageLoadFail when image load fails and should have svg', () => { test('should call onImageLoadFail when image load fails and should have svg', () => {
const wrapper = mount(<Provider store={store}><SizeAwareImage {...baseProps}/></Provider>); const wrapper = mount(<Provider store={store}><SizeAwareImage {...baseProps}/></Provider>);
const errorEvent = {
wrapper.find(SizeAwareImage).find('img').prop('onError')(); target: {},
currentTarget: {},
preventDefault: () => {},
stopPropagation: () => {},
} as React.SyntheticEvent<HTMLImageElement>;
wrapper.find(SizeAwareImage).find('img').prop('onError')?.(errorEvent);
expect(wrapper.find(SizeAwareImage).state('error')).toBe(true); expect(wrapper.find(SizeAwareImage).state('error')).toBe(true);
expect(wrapper.find(SizeAwareImage).find('svg').exists()).toEqual(true); expect(wrapper.find(SizeAwareImage).find('svg').exists()).toEqual(true);
@@ -147,7 +154,7 @@ describe('components/SizeAwareImage', () => {
expect(wrapper.find('div.small-image__container').exists()).toEqual(true); expect(wrapper.find('div.small-image__container').exists()).toEqual(true);
expect(wrapper.find('div.small-image__container').prop('className')). expect(wrapper.find('div.small-image__container').prop('className')).
toEqual('small-image__container cursor--pointer a11y--active'); toEqual('small-image__container cursor--pointer a11y--active small-image__container--min-width');
}); });
test('should properly set container div width', () => { test('should properly set container div width', () => {

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

@@ -4,12 +4,15 @@
/* eslint-disable @mattermost/use-external-link */ /* eslint-disable @mattermost/use-external-link */
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import type {KeyboardEvent, MouseEvent, SyntheticEvent} from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {DownloadOutlineIcon, LinkVariantIcon, CheckIcon} from '@mattermost/compass-icons/components'; import {DownloadOutlineIcon, LinkVariantIcon, CheckIcon} from '@mattermost/compass-icons/components';
import type {FileInfo} from '@mattermost/types/files';
import type {PostImage} from '@mattermost/types/posts';
import type {ActionFunc} from 'mattermost-redux/types/actions';
import {getFileMiniPreviewUrl} from 'mattermost-redux/utils/file_utils'; import {getFileMiniPreviewUrl} from 'mattermost-redux/utils/file_utils';
import LoadingImagePreview from 'components/loading_image_preview'; import LoadingImagePreview from 'components/loading_image_preview';
@@ -23,83 +26,103 @@ const MIN_IMAGE_SIZE = 48;
const MIN_IMAGE_SIZE_FOR_INTERNAL_BUTTONS = 100; const MIN_IMAGE_SIZE_FOR_INTERNAL_BUTTONS = 100;
const MAX_IMAGE_HEIGHT = 350; const MAX_IMAGE_HEIGHT = 350;
export type Props = {
/*
* The source URL of the image
*/
src: string;
/*
* dimensions object to create empty space required to prevent scroll pop
*/
dimensions?: Partial<PostImage>;
fileInfo?: FileInfo;
/**
* fileURL of the original image
*/
fileURL?: string;
alt?: string;
height?: string;
width?: string;
title?: string;
/*
* Boolean value to pass for showing a loader when image is being loaded
*/
showLoader?: boolean;
/*
* A callback that is called as soon as the image component has a height value
*/
onImageLoaded?: ({height, width}: {height: number; width: number}) => void;
/*
* A callback that is called when image load fails
*/
onImageLoadFail?: () => void;
/*
* Fetch the onClick function
*/
onClick?: (e: (KeyboardEvent<HTMLImageElement> | MouseEvent<HTMLImageElement | HTMLDivElement>), link?: string) => void;
/*
* css classes that can added to the img as well as parent div on svg for placeholder
*/
className?: string;
/*
* Enables the logic of surrounding small images with a bigger container div for better click/tap targeting
*/
handleSmallImageContainer?: boolean;
/**
* Enables copy URL functionality through a button on image hover.
*/
enablePublicLink?: boolean;
/**
* Action to fetch public link of an image from server.
*/
getFilePublicLink?: () => ActionFunc;
/*
* Prevents display of utility buttons when image in a location that makes them inappropriate
*/
hideUtilities?: boolean;
}
type State = {
loaded: boolean;
isSmallImage: boolean;
linkCopiedRecently: boolean;
linkCopyInProgress: boolean;
error: boolean;
imageWidth: number;
}
// SizeAwareImage is a component used for rendering images where the dimensions of the image are important for // SizeAwareImage is a component used for rendering images where the dimensions of the image are important for
// ensuring that the page is laid out correctly. // ensuring that the page is laid out correctly.
export default class SizeAwareImage extends React.PureComponent { export default class SizeAwareImage extends React.PureComponent<Props, State> {
static propTypes = { public heightTimeout = 0;
public mounted = false;
public timeout: NodeJS.Timeout|null = null;
/* constructor(props: Props) {
* The source URL of the image
*/
src: PropTypes.string.isRequired,
/*
* dimensions object to create empty space required to prevent scroll pop
*/
dimensions: PropTypes.object,
fileInfo: PropTypes.object,
/**
* fileURL of the original image
*/
fileURL: PropTypes.string,
/*
* Boolean value to pass for showing a loader when image is being loaded
*/
showLoader: PropTypes.bool,
/*
* A callback that is called as soon as the image component has a height value
*/
onImageLoaded: PropTypes.func,
/*
* A callback that is called when image load fails
*/
onImageLoadFail: PropTypes.func,
/*
* Fetch the onClick function
*/
onClick: PropTypes.func,
/*
* css classes that can added to the img as well as parent div on svg for placeholder
*/
className: PropTypes.string,
/*
* Enables the logic of surrounding small images with a bigger container div for better click/tap targeting
*/
handleSmallImageContainer: PropTypes.bool,
/**
* Enables copy URL functionality through a button on image hover.
*/
enablePublicLink: PropTypes.bool,
/**
* Action to fetch public link of an image from server.
*/
getFilePublicLink: PropTypes.func,
/*
* Prevents display of utility buttons when image in a location that makes them inappropriate
*/
hideUtilities: PropTypes.bool,
};
constructor(props) {
super(props); super(props);
const {dimensions} = props; const {dimensions} = props;
this.state = { this.state = {
loaded: false, loaded: false,
isSmallImage: this.dimensionsAvailable(dimensions) ? this.isSmallImage( isSmallImage: this.dimensionsAvailable(dimensions) ? this.isSmallImage(
dimensions.width, dimensions.height) : false, dimensions?.width ?? 0, dimensions?.height ?? 0) : false,
linkCopiedRecently: false, linkCopiedRecently: false,
linkCopyInProgress: false, linkCopyInProgress: false,
error: false,
imageWidth: 0,
}; };
this.heightTimeout = 0; this.heightTimeout = 0;
@@ -113,17 +136,17 @@ export default class SizeAwareImage extends React.PureComponent {
this.mounted = false; this.mounted = false;
} }
dimensionsAvailable = (dimensions) => { dimensionsAvailable = (dimensions?: Partial<PostImage>) => {
return dimensions && dimensions.width && dimensions.height; return dimensions && dimensions.width && dimensions.height;
}; };
isSmallImage = (width, height) => { isSmallImage = (width: number, height: number) => {
return width < MIN_IMAGE_SIZE || height < MIN_IMAGE_SIZE; return width < MIN_IMAGE_SIZE || height < MIN_IMAGE_SIZE;
}; };
handleLoad = (event) => { handleLoad = (event: SyntheticEvent<HTMLImageElement, Event>) => {
if (this.mounted) { if (this.mounted) {
const image = event.target; const image = event.target as HTMLImageElement;
const isSmallImage = this.isSmallImage(image.naturalWidth, image.naturalHeight); const isSmallImage = this.isSmallImage(image.naturalWidth, image.naturalHeight);
this.setState({ this.setState({
loaded: true, loaded: true,
@@ -147,13 +170,13 @@ export default class SizeAwareImage extends React.PureComponent {
} }
}; };
handleImageClick = (e) => { handleImageClick = (e: MouseEvent<HTMLImageElement>) => {
this.props.onClick?.(e, this.props.src); this.props.onClick?.(e, this.props.src);
}; };
onEnterKeyDown = (e) => { onEnterKeyDown = (e: KeyboardEvent<HTMLImageElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
this.handleImageClick(e); this.props.onClick?.(e, this.props.src);
} }
}; };
@@ -197,7 +220,7 @@ export default class SizeAwareImage extends React.PureComponent {
<img <img
{...props} {...props}
aria-label={ariaLabelImage} aria-label={ariaLabelImage}
tabIndex='0' tabIndex={0}
onClick={this.handleImageClick} onClick={this.handleImageClick}
onKeyDown={this.onEnterKeyDown} onKeyDown={this.onEnterKeyDown}
className={ className={
@@ -387,9 +410,9 @@ export default class SizeAwareImage extends React.PureComponent {
let fallback; let fallback;
if (this.dimensionsAvailable(dimensions) && !this.state.loaded) { if (this.dimensionsAvailable(dimensions) && !this.state.loaded) {
const ratio = dimensions.height > MAX_IMAGE_HEIGHT ? MAX_IMAGE_HEIGHT / dimensions.height : 1; const ratio = (dimensions?.height ?? 0) > MAX_IMAGE_HEIGHT ? MAX_IMAGE_HEIGHT / (dimensions?.height ?? 1) : 1;
const height = dimensions.height * ratio; const height = (dimensions?.height ?? 0) * ratio;
const width = dimensions.width * ratio; const width = (dimensions?.width ?? 0) * ratio;
const miniPreview = getFileMiniPreviewUrl(fileInfo); const miniPreview = getFileMiniPreviewUrl(fileInfo);
@@ -397,13 +420,13 @@ export default class SizeAwareImage extends React.PureComponent {
fallback = ( fallback = (
<div <div
className={`image-loading__container ${this.props.className}`} className={`image-loading__container ${this.props.className}`}
style={{maxWidth: dimensions.width}} style={{maxWidth: dimensions?.width}}
> >
<img <img
aria-label={ariaLabelImage} aria-label={ariaLabelImage}
className={this.props.className} className={this.props.className}
src={miniPreview} src={miniPreview}
tabIndex='0' tabIndex={0}
height={height} height={height}
width={width} width={width}
/> />
@@ -454,7 +477,7 @@ export default class SizeAwareImage extends React.PureComponent {
}, 1500); }, 1500);
}; };
copyLinkToAsset = () => { copyLinkToAsset = async () => {
// if linkCopyInProgress is true return // if linkCopyInProgress is true return
if (this.state.linkCopyInProgress !== true) { if (this.state.linkCopyInProgress !== true) {
// set linkCopyInProgress to true to prevent multiple api calls // set linkCopyInProgress to true to prevent multiple api calls
@@ -468,11 +491,12 @@ export default class SizeAwareImage extends React.PureComponent {
} }
// copying public link to clipboard // copying public link to clipboard
this.props.getFilePublicLink().then((data) => { if (this.props.getFilePublicLink) {
const fileURL = data.data.link; const data: any = await this.props.getFilePublicLink();
const fileURL = data.data?.link;
copyToClipboard(fileURL ?? ''); copyToClipboard(fileURL ?? '');
this.startCopyTimer(); this.startCopyTimer();
}); }
} }
}; };