Skip to content
Snippets Groups Projects
Commit ac602116 authored by Tom Robinson's avatar Tom Robinson
Browse files

Add Modal and ModalWithTrigger, fixes numerous modal issues

parent b380b069
Branches
Tags
No related merge requests found
Showing
with 198 additions and 160 deletions
'use strict';
import Modal from 'metabase/components/Modal.react';
import ModalBody from "metabase/components/ModalBody.react";
import SortableItemList from 'metabase/components/SortableItemList.react';
import moment from 'moment';
......@@ -41,7 +41,7 @@ export default React.createClass({
render: function() {
return (
<Modal
<ModalBody
title="Add Question to Dashboard"
closeFn={this.props.closeFn}
>
......@@ -49,7 +49,7 @@ export default React.createClass({
items={this.state.dashboards}
onClickItemFn={this.addToDashboard}
/>
</Modal>
</ModalBody>
);
}
});
......@@ -2,7 +2,7 @@
import FormField from "metabase/components/FormField.react";
import Icon from "metabase/components/Icon.react";
import Modal from 'metabase/components/Modal.react';
import ModalBody from "metabase/components/ModalBody.react";
var cx = React.addons.classSet;
......@@ -82,7 +82,7 @@ export default React.createClass({
);
return (
<Modal
<ModalBody
title="Create Dashboard"
closeFn={this.props.closeFn}
>
......@@ -110,7 +110,7 @@ export default React.createClass({
{formError}
</div>
</form>
</Modal>
</ModalBody>
);
}
});
'use strict';
import Modal from 'metabase/components/Modal.react';
import ModalBody from "metabase/components/ModalBody.react";
import inflection from "inflection";
import cx from "classnames";
......@@ -39,7 +39,7 @@ export default class DeleteQuestionModal extends React.Component {
var dashboardCount = this.props.card.dashboard_count + " " + inflection.inflect("dashboard", this.props.card.dashboard_count);
return (
<Modal
<ModalBody
title="Delete Question"
closeFn={this.props.closeFn}
>
......@@ -56,7 +56,7 @@ export default class DeleteQuestionModal extends React.Component {
<button className="Button Button--primary ml1" onClick={this.props.closeFn}>No</button>
{formError}
</div>
</Modal>
</ModalBody>
);
}
}
......
......@@ -4,7 +4,7 @@ import React, { Component, PropTypes } from "react";
import ActionButton from "metabase/components/ActionButton.react";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.react";
import Modal from "metabase/components/Modal.react";
import ModalBody from "metabase/components/ModalBody.react";
import moment from "moment";
......@@ -48,13 +48,13 @@ export default class HistoryModal extends Component {
render() {
var { revisions } = this.props;
return (
<Modal
<ModalBody
title="Change History"
closeFn={() => this.props.onClose()}
>
<LoadingAndErrorWrapper loading={!revisions} error={this.state.error}>
{() =>
<div className="pb4">
<div className="pb4 flex-full">
<div className="border-bottom flex px4 py1 text-uppercase text-grey-3 text-bold h5">
<span className="flex-half">When</span>
<span className="flex-half">Who</span>
......@@ -86,7 +86,7 @@ export default class HistoryModal extends Component {
</div>
}
</LoadingAndErrorWrapper>
</Modal>
</ModalBody>
);
}
}
......
......@@ -29,7 +29,7 @@ export default class LoadingAndErrorWrapper extends Component {
render() {
return (
<div className="Dashboard full-height flex flex-row flex-full">
<div className={this.props.className}>
{ this.props.error ?
<div className="wrapper py4 text-brand text-centered flex-full bg-white">
<h2 className="text-normal text-grey-2">{this.getErrorMessage()}</h2>
......
'use strict';
"use strict";
import Icon from "metabase/components/Icon.react";
import OnClickOutside from 'react-onclickoutside';
import React, { Component, PropTypes } from "react";
export default React.createClass({
displayName: "Modal",
propTypes: {
title: React.PropTypes.string.isRequired,
closeFn: React.PropTypes.func.isRequired
},
mixins: [OnClickOutside],
import ModalContent from "./ModalContent.react";
handleClickOutside: function() {
this.props.closeFn();
},
export default class Modal extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentWillMount() {
this._modalElement = document.createElement('span');
document.querySelector('body').appendChild(this._modalElement);
}
componentDidMount() {
this._renderPopover();
}
render: function() {
componentDidUpdate() {
this._renderPopover();
}
componentWillUnmount() {
React.unmountComponentAtNode(this._modalElement);
if (this._modalElement.parentNode) {
this._modalElement.parentNode.removeChild(this._modalElement);
}
}
handleClickOutside() {
if (this.props.onClose) {
this.props.onClose()
}
}
_modalComponent() {
return (
<div className="Modal NewForm flex flex-column">
<div className="Form-header flex align-center">
<h2 className="flex-full">{this.props.title}</h2>
<a href="#" className="text-grey-3 p1" onClick={this.props.closeFn}>
<Icon name='close' width="16px" height="16px"/>
</a>
</div>
<div className="flex-full scroll-y">
<ModalContent handleClickOutside={this.handleClickOutside.bind(this)}>
<div className={this.props.className}>
{this.props.children}
</div>
</div>
</ModalContent>
);
}
});
_renderPopover() {
if (this.props.isOpen) {
// modal is open, lets do this!
React.render(
<div className="Modal-backdrop">
{this._modalComponent()}
</div>
, this._modalElement);
} else {
// if the modal isn't open then actively unmount our popover
React.unmountComponentAtNode(this._modalElement);
}
}
render() {
return <span />;
}
}
Modal.propTypes = {
isOpen: PropTypes.bool
};
Modal.defaultProps = {
className: "Modal",
isOpen: true
};
"use strict";
import React, { Component, PropTypes } from "react";
import Icon from "metabase/components/Icon.react";
export default class ModalBody extends Component {
render() {
return (
<div className="Modal-body NewForm">
<div className="Form-header flex align-center">
<h2 className="flex-full">{this.props.title}</h2>
<a href="#" className="text-grey-3 p1" onClick={this.props.closeFn}>
<Icon name='close' width="16px" height="16px"/>
</a>
</div>
<div>
{this.props.children}
</div>
</div>
);
}
}
ModalBody.propTypes = {
title: PropTypes.string.isRequired,
closeFn: PropTypes.func.isRequired
};
......@@ -9,7 +9,7 @@ import OnClickOutside from 'react-onclickoutside';
var popoverStack = [];
export default React.createClass({
displayName: 'PopoverContent',
displayName: 'ModalContent',
mixins: [OnClickOutside],
componentWillMount: function() {
......@@ -18,7 +18,7 @@ export default React.createClass({
componentDidMount: function() {
// HACK: set the z-index of the parent element to ensure it's always on top
this.getDOMNode().parentNode.style.zIndex = popoverStack.length;
this.getDOMNode().parentNode.style.zIndex = popoverStack.length + 2; // HACK: add 2 to ensure it's in front of main and nav elements
},
componentWillUnmount: function() {
......
'use strict';
import Triggerable from './Triggerable.react';
import Modal from './Modal.react';
export default Triggerable(Modal);
'use strict';
/*global document*/
import PopoverContent from './PopoverContent.react'
import ModalContent from './ModalContent.react'
import Tether from 'tether';
......@@ -10,21 +10,14 @@ export default React.createClass({
getDefaultProps: function() {
return {
tether: true,
isOpen: true
};
},
componentWillMount: function() {
var popoverContainer = document.createElement('span');
if (this.props.tether) {
popoverContainer.className = 'PopoverContainer';
}
this._popoverElement = popoverContainer;
this._popoverElement = document.createElement('span');
this._popoverElement.className = 'PopoverContainer';
// TODO: we probably should put this somewhere other than body because then
// its outside our ng-view and could cause lots of issues
document.querySelector('body').appendChild(this._popoverElement);
},
......@@ -55,11 +48,11 @@ export default React.createClass({
_popoverComponent: function() {
return (
<PopoverContent handleClickOutside={this.handleClickOutside}>
<ModalContent handleClickOutside={this.handleClickOutside}>
<div className={this.props.className}>
{this.props.children}
</div>
</PopoverContent>
</ModalContent>
);
},
......@@ -79,18 +72,17 @@ export default React.createClass({
if (this.props.isOpen) {
// modal is open, lets do this!
React.render(this._popoverComponent(), this._popoverElement);
if (this.props.tether) {
var tetherOptions = (this.props.tetherOptions) ? this.props.tetherOptions : this._tetherOptions();
// NOTE: these must be set here because they relate to OUR component and can't be passed in
tetherOptions.element = this._popoverElement;
tetherOptions.target = this.getDOMNode().parentNode;
if (this._tether !== undefined && this._tether !== null) {
this._tether.setOptions(tetherOptions);
} else {
this._tether = new Tether(tetherOptions);
}
var tetherOptions = this.props.tetherOptions || this._tetherOptions();
// NOTE: these must be set here because they relate to OUR component and can't be passed in
tetherOptions.element = this._popoverElement;
tetherOptions.target = this.getDOMNode().parentNode;
if (this._tether !== undefined && this._tether !== null) {
this._tether.setOptions(tetherOptions);
} else {
this._tether = new Tether(tetherOptions);
}
} else {
// if the modal isn't open then actively unmount our popover
......
'use strict';
import Icon from "metabase/components/Icon.react";
import Modal from 'metabase/components/Modal.react';
import ModalBody from "metabase/components/ModalBody.react";
export default React.createClass({
displayName: "QuestionSavedModal",
......@@ -12,7 +12,7 @@ export default React.createClass({
render: function() {
return (
<Modal
<ModalBody
title="Saved! What now?"
closeFn={this.props.closeFn}
>
......@@ -36,7 +36,7 @@ export default React.createClass({
</li>
</ul>
</div>
</Modal>
</ModalBody>
);
}
});
......@@ -2,7 +2,7 @@
import FormField from "metabase/components/FormField.react";
import Icon from "metabase/components/Icon.react";
import Modal from 'metabase/components/Modal.react';
import ModalBody from "metabase/components/ModalBody.react";
import Query from "metabase/lib/query";
......@@ -94,7 +94,7 @@ export default React.createClass({
var name = this.props.card.name || Query.generateQueryDescription(this.props.card.dataset_query, this.props.tableMetadata);
return (
<Modal
<ModalBody
title="Save Question"
closeFn={this.props.closeFn}
>
......@@ -123,7 +123,7 @@ export default React.createClass({
{formError}
</div>
</form>
</Modal>
</ModalBody>
);
},
});
.Modal,
.modal {
z-index:10000;
.modal, /* used by angular-ui-bootstrap */
.Modal {
position: fixed;
top: 50%;
......@@ -21,35 +21,18 @@
box-shadow: 0 0 6px rgba(0, 0, 0, .12);
outline: none;
}
/* from angular-ui-bootstrap */
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, .32);
}
/* A dark translucent div that covers the whole screen */
.Modal-overlay {
position:absolute;
z-index:9999;
top:0;
left:0;
width:100%;
height:100%;
background-color:#000;
opacity: 0.8;
min-height: 400px;
max-height: 90%;
overflow-y: scroll;
}
.Modal-close {
position: absolute;
top: 3px;
right: 5px;
padding: 5px;
cursor: pointer;
.modal-backdrop, /* used by angular-ui-bootstrap */
.Modal-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, .6);
}
'use strict';
import Modal from 'metabase/components/Modal.react';
import ModalBody from "metabase/components/ModalBody.react";
import SortableItemList from 'metabase/components/SortableItemList.react';
import { fetchCards, setEditingDashboard, addCardToDashboard } from "../actions";
......@@ -20,7 +20,7 @@ export default class AddToDashSelectQuestionModal extends React.Component {
render() {
return (
<Modal
<ModalBody
title="Add Question to Dashboard"
closeFn={this.props.onClose}
>
......@@ -29,7 +29,7 @@ export default class AddToDashSelectQuestionModal extends React.Component {
onClickItemFn={(card) => this.onAdd(card)}
showIcons={true}
/>
</Modal>
</ModalBody>
);
}
}
......
......@@ -32,7 +32,7 @@ export default class Dashboard extends Component {
let { dashboard } = this.props;
let { error } = this.state;
return (
<LoadingAndErrorWrapper loading={!dashboard} error={error}>
<LoadingAndErrorWrapper className="Dashboard full-height flex flex-row flex-full" loading={!dashboard} error={error}>
{() =>
<div className="full">
<header className="bg-white border-bottom">
......
......@@ -6,6 +6,7 @@ import ReactGridLayout, { Responsive as ResponsiveReactGridLayout } from "react-
import Icon from "metabase/components/Icon.react";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.react";
import DashCard from "./DashCard.react";
import Modal from "metabase/components/Modal.react";
import RemoveFromDashboardModal from "./RemoveFromDashboardModal.react";
import { setDashCardAttributes } from "../actions";
......@@ -77,16 +78,16 @@ export default class DashboardGrid extends React.Component {
renderRemoveModal() {
// can't use PopoverWithTrigger due to strange interaction with ReactGridLayout
if (this.state.removeModalDashCard != null) {
return (
return (
<Modal isOpen={this.state.removeModalDashCard != null}>
<RemoveFromDashboardModal
dispatch={this.props.dispatch}
dashcard={this.state.removeModalDashCard}
dashboard={this.props.dashboard}
onClose={() => this.setState({ removeModalDashCard: null })}
/>
);
}
</Modal>
);
}
// make grid square by getting the container width, then dividing by 6
......
......@@ -3,12 +3,12 @@
import React, { Component, PropTypes } from "react";
import ActionButton from "metabase/components/ActionButton.react";
import Header from "metabase/components/Header.react";
import Icon from "metabase/components/Icon.react";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.react";
import AddToDashSelectQuestionModal from "./AddToDashSelectQuestionModal.react";
import DeleteDashboardModal from "./DeleteDashboardModal.react";
import Header from "metabase/components/Header.react";
import HistoryModal from "metabase/components/HistoryModal.react";
import Icon from "metabase/components/Icon.react";
import ModalWithTrigger from "metabase/components/ModalWithTrigger.react";
import {
setEditingDashboard,
......@@ -89,9 +89,8 @@ export default class DashboardHeader extends Component {
);
}
editingButtons.push(
<PopoverWithTrigger
<ModalWithTrigger
ref="deleteDashboardModal"
tether={false}
triggerClasses="Button Button--small text-uppercase"
triggerElement="Delete"
>
......@@ -101,7 +100,7 @@ export default class DashboardHeader extends Component {
onClose={() => this.refs.deleteDashboardModal.toggle()}
onDelete={() => this.onDeleteDashboard()}
/>
</PopoverWithTrigger>
</ModalWithTrigger>
);
return editingButtons;
}
......@@ -113,9 +112,8 @@ export default class DashboardHeader extends Component {
if (this.props.isEditing) {
buttonSections.push([
<PopoverWithTrigger
<ModalWithTrigger
ref="dashboardHistory"
tether={false}
triggerElement={<Icon name="history" width="16px" height="16px" />}
>
<HistoryModal
......@@ -128,7 +126,7 @@ export default class DashboardHeader extends Component {
onClose={() => this.refs.dashboardHistory.toggle()}
onReverted={() => this.onRevertedRevision()}
/>
</PopoverWithTrigger>
</ModalWithTrigger>
]);
}
......@@ -148,9 +146,8 @@ export default class DashboardHeader extends Component {
var isEmpty = dashboard.ordered_cards.length === 0;
buttonSections.push([
<PopoverWithTrigger
<ModalWithTrigger
ref="addQuestionModal"
tether={false}
triggerElement={<Icon className={cx({ "Icon--pulse": isEmpty })} name="add" width="16px" height="16px" />}
>
<AddToDashSelectQuestionModal
......@@ -159,7 +156,7 @@ export default class DashboardHeader extends Component {
cards={this.props.cards}
onClose={() => this.refs.addQuestionModal.toggle()}
/>
</PopoverWithTrigger>
</ModalWithTrigger>
]);
return buttonSections;
......
'use strict';
import Modal from 'metabase/components/Modal.react';
import ModalBody from "metabase/components/ModalBody.react";
import cx from "classnames";
......@@ -38,7 +38,7 @@ export default class DeleteDashboardModal extends React.Component {
}
return (
<Modal
<ModalBody
title="Delete Dashboard"
closeFn={this.props.onClose}
>
......@@ -51,7 +51,7 @@ export default class DeleteDashboardModal extends React.Component {
<button className="Button Button--primary ml1" onClick={this.props.onClose}>No</button>
{formError}
</div>
</Modal>
</ModalBody>
);
}
}
......
'use strict';
import Modal from 'metabase/components/Modal.react';
import ModalBody from "metabase/components/ModalBody.react";
import Toggle from 'metabase/components/Toggle.react';
import SortableItemList from 'metabase/components/SortableItemList.react';
......@@ -53,7 +53,7 @@ export default class RemoveFromDashboardModal extends React.Component {
}
return (
<Modal
<ModalBody
title="Remove from Dashboard"
closeFn={() => this.props.onClose()}
>
......@@ -66,7 +66,7 @@ export default class RemoveFromDashboardModal extends React.Component {
<button className="Button Button--danger" onClick={() => this.onRemove()}>Yes</button>
<button className="Button Button--primary ml1" onClick={this.props.onClose}>No</button>
</div>
</Modal>
</ModalBody>
);
}
}
......
......@@ -9,7 +9,8 @@ import DeleteQuestionModal from '../components/DeleteQuestionModal.react';
import Header from "metabase/components/Header.react";
import HistoryModal from "metabase/components/HistoryModal.react";
import Icon from "metabase/components/Icon.react";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.react";
import Modal from "metabase/components/Modal.react";
import ModalWithTrigger from "metabase/components/ModalWithTrigger.react";
import QueryModeToggle from './query_mode_toggle.react';
import QuestionSavedModal from '../components/QuestionSavedModal.react';
import SaveQuestionModal from '../components/SaveQuestionModal.react';
......@@ -137,9 +138,8 @@ export default React.createClass({
if (this.props.cardIsNewFn() && this.props.cardIsDirtyFn()) {
buttons.push(
<PopoverWithTrigger
<ModalWithTrigger
ref="saveModal"
tether={false}
triggerClasses="h4 px1 text-grey-4 text-brand-hover text-uppercase"
triggerElement="Save"
>
......@@ -149,15 +149,14 @@ export default React.createClass({
saveFn={this.saveCard}
closeFn={() => this.refs.saveModal.toggle()}
/>
</PopoverWithTrigger>
</ModalWithTrigger>
);
}
if (!this.props.cardIsNewFn()) {
buttons.push(
<PopoverWithTrigger
<ModalWithTrigger
ref="cardHistory"
tether={false}
triggerElement={<Icon name="history" width="16px" height="16px" />}
>
<HistoryModal
......@@ -169,7 +168,7 @@ export default React.createClass({
onClose={() => this.refs.cardHistory.toggle()}
onReverted={this.onRevertedRevision}
/>
</PopoverWithTrigger>
</ModalWithTrigger>
);
}
......@@ -223,9 +222,8 @@ export default React.createClass({
);
}
editingButtons.push(
<PopoverWithTrigger
<ModalWithTrigger
ref="deleteModal"
tether={false}
triggerClasses="Button Button--small text-uppercase"
triggerElement="Delete"
>
......@@ -234,33 +232,11 @@ export default React.createClass({
deleteCardFn={this.deleteCard}
closeFn={() => this.refs.deleteModal.toggle()}
/>
</PopoverWithTrigger>
</ModalWithTrigger>
);
return editingButtons;
},
getModal: function() {
if (this.state.modal === "saved") {
return (
<QuestionSavedModal
addToDashboardFn={() => this.setState({ modal: "add-to-dashboard" })}
closeFn={() => this.setState({ modal: null })}
/>
);
} else if (this.state.modal === "add-to-dashboard") {
return (
<AddToDashSelectDashModal
card={this.props.card}
dashboardApi={this.props.dashboardApi}
closeFn={() => this.setState({ modal: null })}
notifyCardAddedToDashFn={this.props.notifyCardAddedToDashFn}
/>
);
} else {
return null;
}
},
render: function() {
var subtitleText;
if (this.props.card) {
......@@ -283,7 +259,20 @@ export default React.createClass({
editingButtons={this.getEditingButtons()}
setItemAttributeFn={this.setCardAttribute}
>
{this.getModal()}
<Modal isOpen={this.state.modal === "saved"}>
<QuestionSavedModal
addToDashboardFn={() => this.setState({ modal: "add-to-dashboard" })}
closeFn={() => this.setState({ modal: null })}
/>
</Modal>
<Modal isOpen={this.state.modal === "add-to-dashboard"}>
<AddToDashSelectDashModal
card={this.props.card}
dashboardApi={this.props.dashboardApi}
closeFn={() => this.setState({ modal: null })}
notifyCardAddedToDashFn={this.props.notifyCardAddedToDashFn}
/>
</Modal>
</Header>
);
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment