Unverified Commit 6b324c8b authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

Adds option to open Dashboard preview image into a modal (#460)

* Adding tooltip and testing

* Adding modal and tests

* Adds hover feedback, limits modal size and cleans up tooltip code

* Retrieving site.webmanifest file names and adding crossorigin attribute

* Adds Tamika's comments
parent 266f78c3
......@@ -4,4 +4,4 @@ Please view the dashboard at';
export const PREVIEW_BASE = '/api/dashboard_preview/v0/dashboard';
export const PREVIEW_END = 'preview.jpg';
export const LOADING_TEXT = '...loading dashboard preview...'
export const DASHBOARD_PREVIEW_MODAL_TITLE = 'Dashboard Preview'
import * as React from 'react';
import { Modal } from 'react-bootstrap';
import { shallow } from 'enzyme';
import { mount } from 'enzyme';
import Linkify from 'react-linkify'
......@@ -18,7 +19,7 @@ describe('ImagePreview', () => {
...propOverrides,
};
const wrapper = shallow<ImagePreview>(<ImagePreview {...props} />)
const wrapper = mount<ImagePreview>(<ImagePreview {...props} />)
return { props, wrapper };
};
......@@ -54,7 +55,7 @@ describe('ImagePreview', () => {
});
describe('render', () => {
describe('if no error', () => {
describe('when no error', () => {
describe('when loading', () => {
let wrapper;
beforeAll(() => {
......@@ -71,29 +72,82 @@ describe('ImagePreview', () => {
});
});
describe('when not loading', () => {
describe('when loaded', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props
wrapper = setupResult.wrapper;
wrapper.instance().setState({ isLoading: false, hasError:false });
wrapper.instance().setState({ isLoading: false, hasError: false });
wrapper.update();
});
it('renders visible img with correct props', () => {
const elementProps = wrapper.find('img').props();
expect(elementProps.style).toEqual({ visibility: 'visible' });
expect(elementProps.src).toEqual(`${Constants.PREVIEW_BASE}/${props.uri}/${Constants.PREVIEW_END}`);
expect(elementProps.onLoad).toBe(wrapper.instance().onSuccess);
expect(elementProps.onError).toBe(wrapper.instance().onError);
});
it('renders a button', () => {
const expected = 1;
const actual = wrapper.find('.preview-button').length;
expect(actual).toEqual(expected);
});
})
});
it('renders link if hasError', () => {
const { props, wrapper } = setup();
wrapper.instance().setState({ hasError: true });
expect(wrapper.find(Linkify).exists()).toBeTruthy();
describe('when there is an error', () => {
it('renders a link', () => {
const { props, wrapper } = setup();
wrapper.instance().setState({ hasError: true });
wrapper.update();
expect(wrapper.find(Linkify).exists()).toBeTruthy();
});
});
});
describe('lifecycle', () => {
let wrapper;
describe('when clicking on the dashboard preview button', () => {
beforeAll(() => {
const setupResult = setup();
wrapper = setupResult.wrapper;
wrapper.instance().setState({ isLoading: false, hasError:false });
});
it('should open a modal', () => {
const expected = 1;
let actual;
wrapper.find('.preview-button').simulate('click');
actual = wrapper.find(Modal).length;
expect(actual).toEqual(expected);
});
describe('when closing the modal', () => {
it('should remove the modal markup', () => {
const expected = 0;
let actual;
wrapper.find('.preview-button').simulate('click');
wrapper.find('.modal-header .close').simulate('click');
actual = wrapper.find(Modal).length;
expect(actual).toEqual(expected);
});
});
});
});
});
import * as React from 'react';
import Linkify from 'react-linkify'
import * as React from "react";
import Linkify from "react-linkify";
import { Modal } from "react-bootstrap";
import ShimmeringDashboardLoader from '../ShimmeringDashboardLoader';
import ShimmeringDashboardLoader from "../ShimmeringDashboardLoader";
import * as Constants from './constants';
import './styles.scss';
import * as Constants from "./constants";
import "./styles.scss";
export interface ImagePreviewProps {
uri: string;
......@@ -14,17 +15,43 @@ export interface ImagePreviewProps {
interface ImagePreviewState {
isLoading: boolean;
hasError: boolean;
isModalVisible: boolean;
}
export class ImagePreview extends React.Component<ImagePreviewProps, ImagePreviewState> {
constructor(props) {
super(props);
type PreviewModalProps = {
imageSrc: string,
onClose: () => void,
}
this.state = {
isLoading: true,
hasError: false,
}
}
const PreviewModal = ({ imageSrc, onClose }: PreviewModalProps) => {
const [show, setShow] = React.useState(true);
const handleClose = () => {
setShow(false);
onClose();
};
return (
<Modal show={show} onHide={handleClose} scrollable="true" className="dashboard-preview-modal">
<Modal.Header closeButton={true}>
<Modal.Title className="text-center">Constants.DASHBOARD_PREVIEW_MODAL_TITLE</Modal.Title>
</Modal.Header>
<Modal.Body>
<img
src={imageSrc}
height="auto"
width="100%"
/>
</Modal.Body>
</Modal>
);
};
export class ImagePreview extends React.Component< ImagePreviewProps, ImagePreviewState > {
state = {
isLoading: true,
hasError: false,
isModalVisible: false,
};
onSuccess = () => {
this.setState({ isLoading: false, hasError: false });
......@@ -34,36 +61,56 @@ export class ImagePreview extends React.Component<ImagePreviewProps, ImagePrevie
this.setState({ isLoading: false, hasError: true });
}
render = () => {
handlePreviewButton = () => {
this.setState({ isModalVisible: true });
}
handlePreviewModalClose = () => {
this.setState({ isModalVisible: false });
}
render = () => {
const { uri, redirectUrl } = this.props;
const { isLoading, hasError, isModalVisible } = this.state;
const imageSrc = `${Constants.PREVIEW_BASE}/${uri}/${Constants.PREVIEW_END}`;
return (
<div className='image-preview'>
{
this.state.isLoading &&
<div className="text-placeholder">
<ShimmeringDashboardLoader />
</div>
}
{
!this.state.hasError &&
<img
className='preview'
style={this.state.isLoading ? { visibility: 'hidden' } : { visibility: 'visible' }}
src={`${Constants.PREVIEW_BASE}/${this.props.uri}/${Constants.PREVIEW_END}`}
onLoad={this.onSuccess}
onError={this.onError}
height="auto"
width="100%"
<div className="image-preview">
{isLoading && (
<ShimmeringDashboardLoader />
)}
{!hasError && (
<button className="preview-button" type="button" onClick={this.handlePreviewButton}>
<img
className="preview"
style={
isLoading
? { visibility: "hidden" }
: { visibility: "visible" }
}
src={imageSrc}
onLoad={this.onSuccess}
onError={this.onError}
height="auto"
width="100%"
/>
</button>
)}
{hasError && (
<Linkify
className="body-placeholder"
properties={{ target: "_blank", rel: "noopener noreferrer" }}
>{`${Constants.ERROR_MESSAGE} ${redirectUrl}`}</Linkify>
)}
{isModalVisible && (
<PreviewModal
imageSrc={imageSrc}
onClose={this.handlePreviewModalClose}
/>
}
{
this.state.hasError &&
<Linkify className='body-placeholder' properties={{ target: '_blank', rel:'noopener noreferrer' }} >{`${Constants.ERROR_MESSAGE} ${redirectUrl}`}</Linkify>
}
)}
</div>
);
}
};
}
export default ImagePreview;
@import 'variables';
$max-modal-height: 87vh;
.preview-button {
border: none;
padding: 0;
&:hover img.preview {
cursor: zoom-in;
border: 1px solid $gray50;
}
}
.image-preview {
text-align: center;
.loading-spinner {
......@@ -14,3 +26,7 @@
border: 1px solid $stroke;
}
}
.dashboard-preview-modal .modal-body{
max-height: $max-modal-height;
}
import * as React from 'react';
import { shallow } from 'enzyme';
import { OverlayTrigger, Popover } from 'react-bootstrap';
import EditableSection, { EditableSectionProps } from '.';
import TagInput from 'components/Tags/TagInput';
import { ResourceType } from 'interfaces/Resources';
......
......@@ -4,7 +4,7 @@ import SanitizedHTML from 'react-sanitized-html';
import { shallow } from 'enzyme';
import { OverlayTrigger, Popover } from 'react-bootstrap';
import InfoButton, { InfoButtonProps } from '../';
import InfoButton, { InfoButtonProps } from '.';
describe('InfoButton', () => {
let props: InfoButtonProps;
......
......@@ -2,7 +2,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicons/dev/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicons/dev/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicons/dev/favicon-16x16.png">
<link rel="manifest" href="/static/images/favicons/dev/manifest.json">
<link rel="manifest" href="/static/images/favicons/dev/site.webmanifest" crossorigin="use-credentials">
<link rel="mask-icon" href="/static/images/favicons/dev/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="/static/images/favicons/dev/favicon.ico">
<meta name="msapplication-TileColor" content="#2d89ef">
......
......@@ -2,7 +2,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicons/prod/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicons/prod/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicons/prod/favicon-16x16.png">
<link rel="manifest" href="/static/images/favicons/prod/manifest.json">
<link rel="manifest" href="/static/images/favicons/prod/site.webmanifest" crossorigin="use-credentials">
<link rel="mask-icon" href="/static/images/favicons/prod/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="/static/images/favicons/prod/favicon.ico">
<meta name="msapplication-TileColor" content="#2d89ef">
......
......@@ -2,7 +2,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicons/staging/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicons/staging/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicons/staging/favicon-16x16.png">
<link rel="manifest" href="/static/images/favicons/staging/manifest.json">
<link rel="manifest" href="/static/images/favicons/staging/site.webmanifest" crossorigin="use-credentials">
<link rel="mask-icon" href="/static/images/favicons/staging/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="/static/images/favicons/staging/favicon.ico">
<meta name="msapplication-TileColor" content="#2b5797">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment