Skip to content
Snippets Groups Projects
Commit 7d49b7ef authored by Allen Gilliland's avatar Allen Gilliland
Browse files

add to dashboard button + modal.

parent 12550817
Branches
Tags
No related merge requests found
......@@ -66,8 +66,8 @@ CardControllers.controller('CardList', ['$scope', '$location', 'Card', function(
}]);
CardControllers.controller('CardDetail', [
'$scope', '$routeParams', '$location', 'Card', 'Query', 'CorvusFormGenerator', 'Metabase', 'VisualizationSettings', 'QueryUtils',
function($scope, $routeParams, $location, Card, Query, CorvusFormGenerator, Metabase, VisualizationSettings, QueryUtils) {
'$scope', '$routeParams', '$location', 'Card', 'Dashboard', 'Query', 'CorvusFormGenerator', 'Metabase', 'VisualizationSettings', 'QueryUtils',
function($scope, $routeParams, $location, Card, Dashboard, Query, CorvusFormGenerator, Metabase, VisualizationSettings, QueryUtils) {
/*
HERE BE DRAGONS
......@@ -126,13 +126,15 @@ CardControllers.controller('CardDetail', [
var headerModel = {
card: null,
dashboardApi: Dashboard,
saveFn: function(settings) {
card.name = settings.name;
card.description = settings.description;
// TODO: set permissions here
card.public_perms = settings.public_perms;
var apiCall;
if (card.id !== undefined) {
Card.update(card, function (updatedCard) {
apiCall = Card.update(card, function (updatedCard) {
// TODO: any reason to overwrite card and re-render?
}, function (error) {
console.log('error updating card', error);
......@@ -141,16 +143,17 @@ CardControllers.controller('CardDetail', [
// set the organization
card.organization = $scope.currentOrg.id;
Card.create(card, function (newCard) {
apiCall = Card.create(card, function (newCard) {
$location.path('/' + $scope.currentOrg.slug + '/card/' + newCard.id);
}, function (error) {
console.log('error creating card', error);
});
}
return apiCall.$promise;
},
setPermissions: function(permission) {
card.public_perms = permission;
renderHeader();
notifyCardSavedFn: function(savedCard) {
// header is telling us
},
setQueryModeFn: function(mode) {
var queryTemplate = angular.copy(newQueryTemplates[mode]);
......
......@@ -4,55 +4,61 @@
var AddToDashboard = React.createClass({
displayName: 'AddToDashboard',
propTypes: {
card: React.PropTypes.object.isRequired
// description: React.PropTypes.string,
// hasChanged: React.PropTypes.bool,
// name: React.PropTypes.string,
// permissions: React.PropTypes.number,
// setPermissions: React.PropTypes.func.isRequired,
// save: React.PropTypes.func.isRequired
card: React.PropTypes.object.isRequired,
dashboardApi: React.PropTypes.func.isRequired
},
mixins: [OnClickOutside],
getInitialState: function () {
return {
modalOpen: false,
triggerAction: this._openModal
modalOpen: false
};
},
handleClickOutside: function () {
this.replaceState(this.getInitialState());
},
_openModal: function () {
toggleModal: function () {
var modalOpen = !this.state.modalOpen;
this.setState({
modalOpen: true,
triggerAction: this._save
}, function () {
// focus the name field
this.refs.name.getDOMNode().focus();
modalOpen: modalOpen
});
},
_save: function () {
// var name = this.refs.name.getDOMNode().value,
// description = this.refs.description.getDOMNode().value;
// this.props.save({
// name: name,
// description: description
// });
// // reset the modal
// this.setState({
// modalOpen: false,
// triggerAction: this._openModal
// });
},
render: function () {
// if we don't have a saved card then don't render anything
if (this.props.card.id === undefined) {
return false;
}
// we are rendering a button & controlling a modal popover associated with the button
var modal;
if (this.state.modalOpen) {
var tetherOptions = {
attachment: 'top left',
targetAttachment: 'bottom left',
targetOffset: '20px -150px',
optimizations: {
moveElement: false // always moves to <body> anyway!
}
};
modal = (
<Popover
className="bg-white bordered rounded"
tetherOptions={tetherOptions}
closePopoverFn={this.toggleModal}>
<AddToDashboardPopover
card={this.props.card}
dashboardApi={this.props.dashboardApi}
/>
</Popover>
);
}
// TODO: if our card is dirty should we disable this button?
// ex: someone modifies a query but hasn't run/save the change?
return (
<button className="Button Button--primary">Add to Dash</button>
<span>
{modal}
<button className="Button Button--primary" onClick={this.toggleModal}>Add to Dash</button>
</span>
);
}
});
'use strict';
/*global cx, OnClickOutside, SelectionModule*/
var AddToDashboardPopover = React.createClass({
displayName: 'AddToDashboardPopover',
propTypes: {
card: React.PropTypes.object.isRequired,
dashboardApi: React.PropTypes.func.isRequired
},
getInitialState: function () {
this.loadDashboardList();
return {
dashboards: null,
isCreating: false,
errors: null
};
},
loadDashboardList: function() {
var component = this;
this.props.dashboardApi.list({
'orgId': this.props.card.organization.id,
'filterMode': 'all'
}, function(result) {
component.setState({
dashboards: result
});
}, function(error) {
// TODO: do something relevant here
});
},
toggleCreate: function() {
var state = this.getInitialState();
state.dashboards = this.state.dashboards;
state.isCreating = !this.state.isCreating;
this.replaceState(state);
},
addToExistingDash: function(dashboard, newDash) {
console.log('nd=', newDash);
var isNewDash = (newDash !== undefined) ? newDash : false;
var component = this;
this.props.dashboardApi.addcard({
'dashId': dashboard.id,
'cardId': this.props.card.id
}, function(result) {
if (isNewDash) {
component.setState({
isCreating: false,
errors: null,
newDashSuccess: dashboard
});
} else {
console.log('booyah');
component.setState({
isCreating: false,
errors: null,
existingDashSuccess: dashboard
});
}
}, function(error) {
component.setState({
errors: error
});
});
},
createNewDash: function(event) {
event.preventDefault();
var name = this.refs.name.getDOMNode().value.trim();
var description = this.refs.description.getDOMNode().value.trim();
var perms = this.refs.public_perms.getDOMNode().value;
// populate a new Dash object
var newDash = {
'organization': this.props.card.organization.id,
'name': (name && name.length > 0) ? name : null,
'description': (description && description.length > 0) ? name : null,
'public_perms': 0
};
// create a new dashboard, then add the card to that
var component = this;
this.props.dashboardApi.create(newDash, function(result) {
component.addToExistingDash(result, true);
}, function(error) {
component.setState({
errors: error
});
});
},
renderDashboardsList: function() {
var dashboardsList = [];
if (this.state.dashboards) {
for (var i=0; i < this.state.dashboards.length; i++) {
var dash = this.state.dashboards[i];
dashboardsList.push((<li onClick={this.addToExistingDash.bind(null, dash, false)}>{dash.name}</li>))
}
}
return (
<div>
<h3>Dashboards</h3>
<ul>
{dashboardsList}
</ul>
<div>
<button onClick={this.toggleCreate}>Create a new dashboard</button>
</div>
</div>
);
},
renderCreateDashboardForm: function() {
// TODO: hard coding values :(
var privacyOptions = [
(<option key="0" value="0">Private</option>),
(<option key="1" value="1">Others can read</option>),
(<option key="2" value="2">Others can modify</option>)
];
var formError;
if (this.state.errors) {
var errorMessage = "Server error encountered";
if (this.state.errors.data &&
this.state.errors.data.message) {
errorMessage = this.state.errors.data.message;
}
// TODO: timeout display?
formError = (
<span className="text-error px2">{errorMessage}</span>
);
}
var buttonClasses = cx({
"Button": true,
"Button--primary": true
});
return (
<form className="Form-new" onSubmit={this.createNewDash}>
<div className="Form-offset">
<h3>Create a new dashboard</h3>
<a onClick={this.toggleCreate}>X</a>
</div>
<FormField
displayName="Name"
fieldName="name"
showCharm={true}
errors={this.state.errors}>
<input ref="name" className="Form-input Form-offset full" name="name" placeholder="What is the name of your dashboard?" autofocus/>
</FormField>
<FormField
displayName="Description (optional)"
fieldName="description"
showCharm={true}
errors={this.state.errors}>
<input ref="description" className="Form-input Form-offset full" name="description" placeholder="What else should people know about this?" />
</FormField>
<FormField
displayName="Visibility"
fieldName="public_perms"
showCharm={false}
errors={this.state.errors}>
<label className="Select Form-offset">
<select ref="public_perms">
{privacyOptions}
</select>
</label>
</FormField>
<div className="Form-actions">
<button className={buttonClasses}>
Save
</button>
{formError}
</div>
</form>
);
},
render: function() {
if (this.state.isCreating) {
return this.renderCreateDashboardForm();
} else if (this.state.newDashSuccess) {
var dashDetails = this.state.newDashSuccess;
var dashLink = "/"+this.props.card.organization.slug+"/dash/"+dashDetails.id;
return (
<div>
<p>Your dashboard, {dashDetails.name} was created and {this.props.card.name} was added.</p>
<p><a href={dashLink}>Let me check it out.</a></p>
</div>
);
} else if (this.state.existingDashSuccess) {
var dashDetails = this.state.existingDashSuccess;
var dashLink = "/"+this.props.card.organization.slug+"/dash/"+dashDetails.id;
return (
<div>
<p>{this.props.card.name} was added to {dashDetails.name}</p>
<p><a href={dashLink}>Let me check it out.</a></p>
</div>
);
} else {
return this.renderDashboardsList();
}
}
});
'use strict';
/*global cx, OnClickOutside, SelectionModule*/
var FormField = React.createClass({
displayName: 'FormField',
propTypes: {
fieldName: React.PropTypes.string.isRequired,
displayName: React.PropTypes.string.isRequired,
showCharm: React.PropTypes.bool,
errors: React.PropTypes.object
},
extractFieldError: function() {
if (this.props.errors &&
this.props.errors.data.errors &&
this.props.fieldName in this.props.errors.data.errors) {
return this.props.errors.data.errors[this.props.fieldName];
} else {
return null;
}
},
render: function() {
var fieldError = this.extractFieldError();
var fieldClasses = cx({
"Form-field": true,
"Form--fieldError": (fieldError !== null)
});
var fieldErrorMessage;
if (fieldError !== null) {
fieldErrorMessage = (
<span>{fieldError}</span>
);
}
var fieldLabel = (
<label className="Form-label Form-offset">{this.props.displayName}: {fieldErrorMessage}</label>
);
var formCharm;
if (this.props.showCharm) {
formCharm = (
<span className="Form-charm"></span>
);
}
return (
<div className={fieldClasses}>
{fieldLabel}
{this.props.children}
{formCharm}
</div>
);
}
});
......@@ -11,6 +11,7 @@ var QueryHeader = React.createClass({
displayName: 'QueryHeader',
propTypes: {
card: React.PropTypes.object.isRequired,
dashboardApi: React.PropTypes.func.isRequired,
queryType: React.PropTypes.string,
saveFn: React.PropTypes.func.isRequired,
setQueryModeFn: React.PropTypes.func.isRequired,
......@@ -53,6 +54,7 @@ var QueryHeader = React.createClass({
{downloadButton}
<AddToDashboard
card={this.props.card}
dashboardApi={this.props.dashboardApi}
/>
<QueryModeToggle
card={this.props.card}
......
'use strict';
/*global cx, Tether*/
var Popover = React.createClass({
displayName: 'Popover',
componentWillMount: function() {
var popoverContainer = document.createElement('span');
popoverContainer.className = 'PopoverContainer';
this._popoverElement = 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);
},
componentDidMount: function() {
this._renderPopover();
},
componentDidUpdate: function() {
this._renderPopover();
},
_popoverComponent: function() {
var className = this.props.className;
return (
<div className={className}>
{this.props.children}
</div>
);
},
_tetherOptions: function() {
// sensible defaults for most popovers
return {
attachment: 'top left',
targetAttachment: 'bottom left',
targetOffset: '10px 0',
optimizations: {
moveElement: false // always moves to <body> anyway!
}
};
},
_renderPopover: function() {
React.render(this._popoverComponent(), this._popoverElement);
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().parentElement;
if (this._tether != null) {
this._tether.setOptions(tetherOptions);
} else {
this._tether = new Tether(tetherOptions);
}
},
componentWillUnmount: function() {
this._tether.destroy();
React.unmountComponentAtNode(this._popoverElement);
if (this._popoverElement.parentNode) {
this._popoverElement.parentNode.removeChild(this._popoverElement);
}
},
render: function() {
return <span/>;
}
});
\ No newline at end of file
......@@ -34,6 +34,10 @@ var Saver = React.createClass({
permissions: permission
});
},
isFormReady: function () {
// TODO: make this work properly
return true;
},
_save: function () {
var name = this.refs.name.getDOMNode().value,
description = this.refs.description.getDOMNode().value,
......@@ -51,17 +55,22 @@ var Saver = React.createClass({
});
},
render: function () {
if (!this.props.card.isDirty()) {
return false;
}
var buttonClasses = cx({
'SaveButton': true,
'Button': true,
'block': true,
'Button--primary': this.state.modalOpen
'Button--primary': this.isFormReady()
});
var modalClasses = cx({
'SaveModal': true,
'Modal--showing': this.state.modalOpen
});
// TODO: these should be html <option> elements
var privacyOptions = [
{
code: 0,
......@@ -77,35 +86,45 @@ var Saver = React.createClass({
},
];
// default state is false, which means we don't render anything in the DOM
var saver = false;
if (this.props.card.isDirty()) {
saver = (
<div className="SaveWrapper mr2">
<div className={modalClasses}>
<div className="ModalContent">
<input ref="name" type="text" placeholder="Name" autofocus defaultValue={this.props.card.name} />
<input ref="description" type="text" placeholder="Add a description" defaultValue={this.props.card.description}/>
<div className="mt4 ml2 mr2 clearfix">
<span className="text-grey-3 inline-block my1">Privacy:</span>
<div className="float-right">
<SelectionModule
placeholder="Privacy"
items={privacyOptions}
selectedKey='code'
selectedValue={this.props.permissions}
display='display'
action={this._setPermissions}
/>
</div>
</div>
</div>
// <form className="Form-new bordered rounded shadowed">
// <div className="Form-field" mb-form-field="name">
// <mb-form-label display-name="Name" field-name="name"></mb-form-label>
// <input ref="name" className="Form-input Form-offset full" name="name" placeholder="What is the name of your dashboard?" defaultValue={this.props.card.name} autofocus/>
// <span className="Form-charm"></span>
// </div>
// <div className="Form-field" mb-form-field="description">
// <mb-form-label display-name="Description" field-name="description"></mb-form-label>
// <input ref="description" className="Form-input Form-offset full" name="description" placeholder="What else should people know about this?" defaultValue={this.props.card.description} />
// <span className="Form-charm"></span>
// </div>
// <div className="Form-field" mb-form-field="public_perms">
// <mb-form-label display-name="Privacy" field-name="public_perms"></mb-form-label>
// <label className="Select Form-offset">
// <select ref="public_perms">
// {privacyOptions}
// </select>
// </label>
// </div>
// <div className="Form-actions">
// <button className={buttonClasses} onClick={this.save} ng-disabled="!form.$valid">
// Save
// </button>
// <mb-form-message form="form"></mb-form-message>
// </div>
// </form>
return (
<div className="SaveWrapper">
<div className={modalClasses}>
<div className="ModalContent">
</div>
<a className={buttonClasses} onClick={this.state.triggerAction}>Save</a>
</div>
);
}
return saver;
<a className="Button Button--primary" onClick={this.state.triggerAction}>Save</a>
</div>
);
}
});
......@@ -30,7 +30,7 @@
(defendpoint POST "/"
"Create a new `Dashboard`."
[:as {{:keys [organization name public_perms] :as body} :body}]
{name Required
{name [Required NonEmptyString]
organization Required
public_perms [Required PublicPerms]}
(read-check Org organization) ; any user who has permissions for this Org can make a dashboard
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment