[explore] refactor slice action button group (#1074)

* pull explore actions button group into component

* use button component

* make sure we render all action buttons

* test that embed code is correct

* don't need before each

* generalize modal trigger for use with plain links or icons
This commit is contained in:
Alanna Scott
2016-09-20 13:45:27 -07:00
committed by GitHub
parent 32980a653c
commit 0e7af8d8a6
17 changed files with 592 additions and 203 deletions

View File

@@ -0,0 +1,37 @@
import React, { PropTypes } from 'react';
import ModalTrigger from './../../components/ModalTrigger';
const propTypes = {
slice: PropTypes.object.isRequired,
};
export default class DisplayQueryButton extends React.Component {
constructor(props) {
super(props);
this.state = {
viewSqlQuery: '',
};
this.beforeOpen = this.beforeOpen.bind(this);
}
beforeOpen() {
this.setState({
viewSqlQuery: this.props.slice.viewSqlQuery,
});
}
render() {
const modalBody = (<pre>{this.state.viewSqlQuery}</pre>);
return (
<ModalTrigger
isButton
triggerNode={<span>Query</span>}
modalTitle="Query"
modalBody={modalBody}
beforeOpen={this.beforeOpen}
/>
);
}
}
DisplayQueryButton.propTypes = propTypes;

View File

@@ -0,0 +1,105 @@
import React, { PropTypes } from 'react';
import CopyToClipboard from './../../components/CopyToClipboard';
import { Popover, OverlayTrigger } from 'react-bootstrap';
const propTypes = {
slice: PropTypes.object.isRequired,
};
export default class EmbedCodeButton extends React.Component {
constructor(props) {
super(props);
this.state = {
height: '400',
width: '600',
srcLink: window.location.origin + props.slice.data.standalone_endpoint,
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(e) {
const value = e.currentTarget.value;
const name = e.currentTarget.name;
const data = {};
data[name] = value;
this.setState(data);
}
generateEmbedHTML() {
const { width, height, srcLink } = this.state;
/* eslint max-len: 0 */
const embedHTML =
`<iframe src="${srcLink}" width="${width}" height="${height}" seamless frameBorder="0" scrolling="no"></iframe>`;
return embedHTML;
}
renderPopover() {
const html = this.generateEmbedHTML();
return (
<Popover id="embed-code-popover">
<div>
<div className="row">
<div className="col-sm-10">
<textarea name="embedCode" value={html} rows="4" readOnly className="form-control input-sm"></textarea>
</div>
<div className="col-sm-2">
<CopyToClipboard
shouldShowText={false}
text={html}
copyNode={<i className="fa fa-clipboard" title="Copy to clipboard"></i>}
/>
</div>
</div>
<br />
<div className="row">
<div className="col-md-6 col-sm-12">
<div className="form-group">
<small>
<label className="control-label" htmlFor="embed-height">Height</label>
</small>
<input
className="form-control input-sm"
type="text"
defaultValue={this.state.height}
name="height"
onChange={this.handleInputChange}
/>
</div>
</div>
<div className="col-md-6 col-sm-12">
<div className="form-group">
<small>
<label className="control-label" htmlFor="embed-width">Width</label>
</small>
<input
className="form-control input-sm"
type="text"
defaultValue={this.state.width}
name="width"
onChange={this.handleInputChange}
id="embed-width"
/>
</div>
</div>
</div>
</div>
</Popover>
);
}
render() {
return (
<OverlayTrigger
trigger="click"
rootClose
placement="left"
overlay={this.renderPopover()}
>
<span className="btn btn-default btn-sm">
<i className="fa fa-code"></i>&nbsp;
</span>
</OverlayTrigger>
);
}
}
EmbedCodeButton.propTypes = propTypes;

View File

@@ -0,0 +1,46 @@
import React, { PropTypes } from 'react';
import cx from 'classnames';
import URLShortLinkButton from './URLShortLinkButton';
import EmbedCodeButton from './EmbedCodeButton';
import DisplayQueryButton from './DisplayQueryButton';
const propTypes = {
canDownload: PropTypes.string.isRequired,
slice: PropTypes.object.isRequired,
};
export default function ExploreActionButtons({ canDownload, slice }) {
const exportToCSVClasses = cx('btn btn-default btn-sm', {
'disabled disabledButton': !canDownload,
});
return (
<div className="btn-group results" role="group">
<URLShortLinkButton slice={slice} />
<EmbedCodeButton slice={slice} />
<a
href={slice.data.json_endpoint}
className="btn btn-default btn-sm"
title="Export to .json"
target="_blank"
>
<i className="fa fa-file-code-o"></i> .json
</a>
<a
href={slice.data.csv_endpoint}
className={exportToCSVClasses}
title="Export to .csv format"
target="_blank"
>
<i className="fa fa-file-text-o"></i> .csv
</a>
<DisplayQueryButton slice={slice} />
</div>
);
}
ExploreActionButtons.propTypes = propTypes;

View File

@@ -0,0 +1,71 @@
import React, { PropTypes } from 'react';
import { Popover, OverlayTrigger } from 'react-bootstrap';
import CopyToClipboard from './../../components/CopyToClipboard';
import $ from 'jquery';
const propTypes = {
slice: PropTypes.object.isRequired,
};
export default class URLShortLinkButton extends React.Component {
constructor(props) {
super(props);
this.state = {
shortUrl: '',
};
this.getShortUrl();
}
getShortUrl() {
$.ajax({
type: 'POST',
url: '/r/shortner/',
data: {
data: '/' + window.location.pathname + this.props.slice.querystring(),
},
success: (data) => {
this.setState({
shortUrl: data,
});
},
error: (error) => {
/* eslint no-console: 0 */
if (console && console.warn) {
console.warn('Something went wrong...');
console.warn(error);
}
},
});
}
renderPopover() {
return (
<Popover id="shorturl-popover">
<CopyToClipboard
text={this.state.shortUrl}
copyNode={<i className="fa fa-clipboard" title="Copy to clipboard"></i>}
/>
</Popover>
);
}
render() {
const shortUrl = this.state.shortUrl;
const isDisabled = shortUrl === '';
return (
<OverlayTrigger
trigger="click"
rootClose
placement="left"
overlay={this.renderPopover()}
>
<span className="btn btn-default btn-sm" disabled={isDisabled}>
<i className="fa fa-link"></i>&nbsp;
</span>
</OverlayTrigger>
);
}
}
URLShortLinkButton.propTypes = propTypes;

View File

@@ -11,6 +11,7 @@ const jQuery = window.jQuery = require('jquery'); // eslint-disable-line
import React from 'react';
import ReactDOM from 'react-dom';
import QueryAndSaveBtns from './components/QueryAndSaveBtns.jsx';
import ExploreActionButtons from './components/ExploreActionButtons.jsx';
require('jquery-ui');
$.widget.bridge('uitooltip', $.ui.tooltip); // Shutting down jq-ui tooltips
@@ -62,7 +63,6 @@ function query(forceUpdate, pushState) {
force = false;
}
$('.query-and-save button').attr('disabled', 'disabled');
$('.btn-group.results span,a').attr('disabled', 'disabled');
if (force) { // Don't hide the alert message when the page is just loaded
$('div.alert').remove();
}
@@ -160,148 +160,6 @@ function initExploreView() {
px.initFavStars();
function copyURLToClipboard(url) {
const textArea = document.createElement('textarea');
textArea.style.position = 'fixed';
textArea.style.left = '-1000px';
textArea.value = url;
document.body.appendChild(textArea);
textArea.select();
let successful;
try {
successful = document.execCommand('copy');
if (!successful) {
throw new Error('Not successful');
}
} catch (err) {
window.alert('Sorry, your browser does not support copying. Use Ctrl / Cmd + C!'); // eslint-disable-line
}
document.body.removeChild(textArea);
return successful;
}
$('#shortner').click(function () {
$.ajax({
type: 'POST',
url: '/r/shortner/',
data: {
data: '/' + window.location.pathname + slice.querystring(),
},
success(data) {
const close = (
'<a style="cursor: pointer;">' +
'<i class="fa fa-close" id="close_shortner"></i>' +
'</a>'
);
const copy = (
'<a style="cursor: pointer;">' +
'<i class="fa fa-clipboard" title="Copy to clipboard" id="copy_url"></i>' +
'</a>'
);
const spaces = '&nbsp;&nbsp;&nbsp;';
const popover = data + spaces + copy + spaces + close;
const $shortner = $('#shortner')
.popover({
content: popover,
placement: 'left',
html: true,
trigger: 'manual',
})
.popover('show');
function destroyPopover() {
$shortner.popover('destroy');
}
$('#copy_url')
.tooltip()
.click(function () {
const success = copyURLToClipboard(data);
if (success) {
$(this).attr('data-original-title', 'Copied!')
.tooltip('fixTitle')
.tooltip('show');
window.setTimeout(destroyPopover, 1200);
}
});
$('#close_shortner').click(destroyPopover);
},
error(error) {
showModal({
title: 'Error',
body: 'Sorry, an error occurred during this operation:<br/>' + error,
});
},
});
});
$('#standalone').click(function () {
const srcLink = window.location.origin + slice.data.standalone_endpoint;
const close = (
'<a style="cursor: pointer;">' +
'<i class="fa fa-close" id="close_standalone"></i>' +
'</a>'
);
const copy = (
'<a style="cursor: pointer;">' +
'<i class="fa fa-clipboard" title="Copy to clipboard" id="copy_embed"></i>' +
'</a>'
);
const spaces = '&nbsp;&nbsp;&nbsp;';
const widthInput = '<input type="number" id="standalone_width" placeholder="width">';
const heightInput = '<input type="number" id="standalone_height" placeholder="height">';
let popover = "<input id='standalone_text' value='' disabled></input>";
popover = popover + spaces + copy + spaces + close + spaces + widthInput + spaces + heightInput;
let dataToCopy = '';
const $standalone = $(this);
function destroyPopover() {
$standalone.popover('destroy');
}
$standalone.popover({
content: popover,
title: 'embed html',
placement: 'left',
html: true,
trigger: 'manual',
})
.popover('show');
$('#copy_embed').tooltip().click(function () {
const success = copyURLToClipboard(dataToCopy);
if (success) {
$(this).attr('data-original-title', 'Copied!')
.tooltip('fixTitle')
.tooltip('show');
window.setTimeout(destroyPopover, 1200);
}
});
$('#close_standalone').click(destroyPopover);
const $standaloneWidth = $('#standalone_width');
const $standaloneHeight = $('#standalone_height');
const $standaloneText = $('#standalone_text');
function generateEmbedHTML() {
const width = $standaloneWidth.val();
const height = $standaloneHeight.val();
dataToCopy = `<iframe src="${srcLink}" width="${width}" height="${height}"`;
dataToCopy = dataToCopy + ' seamless frameBorder="0" scrolling="no"></iframe>';
$standaloneText.val(dataToCopy);
}
$standaloneHeight.change(function () {
generateEmbedHTML();
});
$standaloneWidth.change(function () {
generateEmbedHTML();
});
generateEmbedHTML();
});
$('#viz_type').change(function () {
$('#query').submit();
});
@@ -386,15 +244,6 @@ function initExploreView() {
addFilter(undefined, 'having');
});
const queryAndSaveBtnsEl = document.getElementById('js-query-and-save-btns');
ReactDOM.render(
<QueryAndSaveBtns
canAdd={queryAndSaveBtnsEl.getAttribute('data-can-add')}
onQuery={() => query(true)}
/>,
queryAndSaveBtnsEl
);
function createChoices(term, data) {
const filtered = $(data).filter(function () {
return this.text.localeCompare(term) === 0;
@@ -487,6 +336,26 @@ function initExploreView() {
prepSaveDialog();
}
function initComponents() {
const queryAndSaveBtnsEl = document.getElementById('js-query-and-save-btns');
ReactDOM.render(
<QueryAndSaveBtns
canAdd={queryAndSaveBtnsEl.getAttribute('data-can-add')}
onQuery={() => query(true)}
/>,
queryAndSaveBtnsEl
);
const exploreActionsEl = document.getElementById('js-explore-actions');
ReactDOM.render(
<ExploreActionButtons
canDownload={exploreActionsEl.getAttribute('data-can-download')}
slice={slice}
/>,
exploreActionsEl
);
}
$(document).ready(function () {
const data = $('.slice').data('slice');
@@ -497,7 +366,10 @@ $(document).ready(function () {
$('.slice').data('slice', slice);
// call vis render method, which issues ajax
// calls render on the slice for the first time
query(false, false);
slice.bindResizeToWindowResize();
initComponents();
});